github/gitlab: many fixes and improvments at the config step

Michael Muré created

Change summary

bridge/core/auth/credential.go |   3 
bridge/github/config.go        | 110 ++++++++++++++++++++++++-----------
bridge/github/import_query.go  |  14 +--
bridge/gitlab/config.go        |  54 +++++++++++-----
bridge/gitlab/export.go        |  10 ++
bridge/gitlab/export_test.go   |   9 +-
bridge/gitlab/import.go        |   6 +
bridge/gitlab/import_test.go   |   5 
8 files changed, 140 insertions(+), 71 deletions(-)

Detailed changes

bridge/core/auth/credential.go 🔗

@@ -18,7 +18,8 @@ const (
 	configKeyCreateTime = "createtime"
 	configKeyPrefixMeta = "meta."
 
-	MetaKeyLogin = "login"
+	MetaKeyLogin   = "login"
+	MetaKeyBaseURL = "base-url"
 )
 
 type CredentialKind string

bridge/github/config.go 🔗

@@ -3,6 +3,7 @@ package github
 import (
 	"bufio"
 	"bytes"
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -70,25 +71,7 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		return nil, fmt.Errorf("invalid parameter owner: %v", owner)
 	}
 
-	login := params.Login
-	if login == "" {
-		validator := func(name string, value string) (string, error) {
-			ok, err := validateUsername(value)
-			if err != nil {
-				return "", err
-			}
-			if !ok {
-				return "invalid login", nil
-			}
-			return "", nil
-		}
-
-		login, err = input.Prompt("Github login", "login", input.Required, validator)
-		if err != nil {
-			return nil, err
-		}
-	}
-
+	var login string
 	var cred auth.Credential
 
 	switch {
@@ -97,10 +80,27 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		if err != nil {
 			return nil, err
 		}
+		l, ok := cred.GetMetadata(auth.MetaKeyLogin)
+		if !ok {
+			return nil, fmt.Errorf("credential doesn't have a login")
+		}
+		login = l
 	case params.TokenRaw != "":
-		cred = auth.NewToken(params.TokenRaw, target)
-		cred.SetMetadata(auth.MetaKeyLogin, login)
+		token := auth.NewToken(params.TokenRaw, target)
+		login, err = getLoginFromToken(token)
+		if err != nil {
+			return nil, err
+		}
+		token.SetMetadata(auth.MetaKeyLogin, login)
+		cred = token
 	default:
+		login = params.Login
+		if login == "" {
+			login, err = input.Prompt("Github login", "login", input.Required, usernameValidator)
+			if err != nil {
+				return nil, err
+			}
+		}
 		cred, err = promptTokenOptions(repo, login, owner, project)
 		if err != nil {
 			return nil, err
@@ -159,6 +159,17 @@ func (*Github) ValidateConfig(conf core.Configuration) error {
 	return nil
 }
 
+func usernameValidator(name string, value string) (string, error) {
+	ok, err := validateUsername(value)
+	if err != nil {
+		return "", err
+	}
+	if !ok {
+		return "invalid login", nil
+	}
+	return "", nil
+}
+
 func requestToken(note, login, password string, scope string) (*http.Response, error) {
 	return requestTokenWith2FA(note, login, password, "", scope)
 }
@@ -231,7 +242,11 @@ func randomFingerprint() string {
 
 func promptTokenOptions(repo repository.RepoConfig, login, owner, project string) (auth.Credential, error) {
 	for {
-		creds, err := auth.List(repo, auth.WithTarget(target), auth.WithMeta(auth.MetaKeyLogin, login))
+		creds, err := auth.List(repo,
+			auth.WithTarget(target),
+			auth.WithKind(auth.KindToken),
+			auth.WithMeta(auth.MetaKeyLogin, login),
+		)
 		if err != nil {
 			return nil, err
 		}
@@ -275,13 +290,7 @@ func promptTokenOptions(repo repository.RepoConfig, login, owner, project string
 
 		switch index {
 		case 1:
-			value, err := promptToken()
-			if err != nil {
-				return nil, err
-			}
-			token := auth.NewToken(value, target)
-			token.SetMetadata(auth.MetaKeyLogin, login)
-			return token, nil
+			return promptToken()
 		case 2:
 			value, err := loginAndRequestToken(login, owner, project)
 			if err != nil {
@@ -296,7 +305,7 @@ func promptTokenOptions(repo repository.RepoConfig, login, owner, project string
 	}
 }
 
-func promptToken() (string, error) {
+func promptToken() (*auth.Token, error) {
 	fmt.Println("You can generate a new token by visiting https://github.com/settings/tokens.")
 	fmt.Println("Choose 'Generate new token' and set the necessary access scope for your repository.")
 	fmt.Println()
@@ -312,14 +321,28 @@ func promptToken() (string, error) {
 		panic("regexp compile:" + err.Error())
 	}
 
+	var login string
+
 	validator := func(name string, value string) (complaint string, err error) {
-		if re.MatchString(value) {
-			return "", nil
+		if !re.MatchString(value) {
+			return "token has incorrect format", nil
+		}
+		login, err = getLoginFromToken(auth.NewToken(value, target))
+		if err != nil {
+			return fmt.Sprintf("token is invalid: %v", err), nil
 		}
-		return "token has incorrect format", nil
+		return "", nil
+	}
+
+	rawToken, err := input.Prompt("Enter token", "token", input.Required, validator)
+	if err != nil {
+		return nil, err
 	}
 
-	return input.Prompt("Enter token", "token", input.Required, validator)
+	token := auth.NewToken(rawToken, target)
+	token.SetMetadata(auth.MetaKeyLogin, login)
+
+	return token, nil
 }
 
 func loginAndRequestToken(login, owner, project string) (string, error) {
@@ -543,3 +566,22 @@ func validateProject(owner, project string, token *auth.Token) (bool, error) {
 
 	return resp.StatusCode == http.StatusOK, nil
 }
+
+func getLoginFromToken(token *auth.Token) (string, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
+	defer cancel()
+
+	client := buildClient(token)
+
+	var q loginQuery
+
+	err := client.Query(ctx, &q, nil)
+	if err != nil {
+		return "", err
+	}
+	if q.Viewer.Login == "" {
+		return "", fmt.Errorf("github say username is empty")
+	}
+
+	return q.Viewer.Login, nil
+}

bridge/github/import_query.go 🔗

@@ -168,14 +168,6 @@ type ghostQuery struct {
 	} `graphql:"user(login: $login)"`
 }
 
-type labelQuery struct {
-	Repository struct {
-		Label struct {
-			ID string `graphql:"id"`
-		} `graphql:"label(name: $label)"`
-	} `graphql:"repository(owner: $owner, name: $name)"`
-}
-
 type labelsQuery struct {
 	Repository struct {
 		Labels struct {
@@ -189,3 +181,9 @@ type labelsQuery struct {
 		} `graphql:"labels(first: $first, after: $after)"`
 	} `graphql:"repository(owner: $owner, name: $name)"`
 }
+
+type loginQuery struct {
+	Viewer struct {
+		Login string `graphql:"login"`
+	} `graphql:"viewer"`
+}

bridge/gitlab/config.go 🔗

@@ -35,9 +35,6 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 	if params.Owner != "" {
 		fmt.Println("warning: --owner is ineffective for a gitlab bridge")
 	}
-	if params.Login != "" {
-		fmt.Println("warning: --login is ineffective for a gitlab bridge")
-	}
 
 	conf := make(core.Configuration)
 	var err error
@@ -53,24 +50,25 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		}
 	}
 
-	var url string
+	var projectURL string
 
 	// get project url
 	switch {
 	case params.URL != "":
-		url = params.URL
+		projectURL = params.URL
 	default:
 		// terminal prompt
-		url, err = promptURL(repo, baseUrl)
+		projectURL, err = promptProjectURL(repo, baseUrl)
 		if err != nil {
 			return nil, errors.Wrap(err, "url prompt")
 		}
 	}
 
-	if !strings.HasPrefix(url, params.BaseURL) {
-		return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, url)
+	if !strings.HasPrefix(projectURL, params.BaseURL) {
+		return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, projectURL)
 	}
 
+	var login string
 	var cred auth.Credential
 
 	switch {
@@ -79,16 +77,30 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		if err != nil {
 			return nil, err
 		}
+		l, ok := cred.GetMetadata(auth.MetaKeyLogin)
+		if !ok {
+			return nil, fmt.Errorf("credential doesn't have a login")
+		}
+		login = l
 	case params.TokenRaw != "":
 		token := auth.NewToken(params.TokenRaw, target)
-		login, err := getLoginFromToken(baseUrl, token)
+		login, err = getLoginFromToken(baseUrl, token)
 		if err != nil {
 			return nil, err
 		}
 		token.SetMetadata(auth.MetaKeyLogin, login)
+		token.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
 		cred = token
 	default:
-		cred, err = promptTokenOptions(repo, baseUrl)
+		login := params.Login
+		if login == "" {
+			// TODO: validate username
+			login, err = input.Prompt("Gitlab login", "login", input.Required)
+			if err != nil {
+				return nil, err
+			}
+		}
+		cred, err = promptTokenOptions(repo, login, baseUrl)
 		if err != nil {
 			return nil, err
 		}
@@ -100,7 +112,7 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 	}
 
 	// validate project url and get its ID
-	id, err := validateProjectURL(baseUrl, url, token)
+	id, err := validateProjectURL(baseUrl, projectURL, token)
 	if err != nil {
 		return nil, errors.Wrap(err, "project validation")
 	}
@@ -122,7 +134,7 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		}
 	}
 
-	return conf, nil
+	return conf, core.FinishConfig(repo, metaKeyGitlabLogin, login)
 }
 
 func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
@@ -176,9 +188,14 @@ func promptBaseUrl() (string, error) {
 	return input.Prompt("Base url", "url", input.Required, validator)
 }
 
-func promptTokenOptions(repo repository.RepoConfig, baseUrl string) (auth.Credential, error) {
+func promptTokenOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) {
 	for {
-		creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
+		creds, err := auth.List(repo,
+			auth.WithTarget(target),
+			auth.WithKind(auth.KindToken),
+			auth.WithMeta(auth.MetaKeyLogin, login),
+			auth.WithMeta(auth.MetaKeyBaseURL, baseUrl),
+		)
 		if err != nil {
 			return nil, err
 		}
@@ -262,11 +279,12 @@ func promptToken(baseUrl string) (*auth.Token, error) {
 
 	token := auth.NewToken(rawToken, target)
 	token.SetMetadata(auth.MetaKeyLogin, login)
+	token.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
 
 	return token, nil
 }
 
-func promptURL(repo repository.RepoCommon, baseUrl string) (string, error) {
+func promptProjectURL(repo repository.RepoCommon, baseUrl string) (string, error) {
 	// remote suggestions
 	remotes, err := repo.GetRemotes()
 	if err != nil {
@@ -317,13 +335,13 @@ func promptURL(repo repository.RepoCommon, baseUrl string) (string, error) {
 			return "", err
 		}
 
-		url := strings.TrimSpace(line)
-		if url == "" {
+		projectURL := strings.TrimSpace(line)
+		if projectURL == "" {
 			fmt.Println("URL is empty")
 			continue
 		}
 
-		return url, nil
+		return projectURL, nil
 	}
 }
 

bridge/gitlab/export.go 🔗

@@ -47,7 +47,7 @@ func (ge *gitlabExporter) Init(repo *cache.RepoCache, conf core.Configuration) e
 	ge.repositoryID = ge.conf[keyProjectID]
 
 	// preload all clients
-	err := ge.cacheAllClient(repo)
+	err := ge.cacheAllClient(repo, ge.conf[keyGitlabBaseUrl])
 	if err != nil {
 		return err
 	}
@@ -55,8 +55,12 @@ func (ge *gitlabExporter) Init(repo *cache.RepoCache, conf core.Configuration) e
 	return nil
 }
 
-func (ge *gitlabExporter) cacheAllClient(repo *cache.RepoCache) error {
-	creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
+func (ge *gitlabExporter) cacheAllClient(repo *cache.RepoCache, baseURL string) error {
+	creds, err := auth.List(repo,
+		auth.WithTarget(target),
+		auth.WithKind(auth.KindToken),
+		auth.WithMeta(auth.MetaKeyBaseURL, baseURL),
+	)
 	if err != nil {
 		return err
 	}

bridge/gitlab/export_test.go 🔗

@@ -164,6 +164,7 @@ func TestPushPull(t *testing.T) {
 
 	token := auth.NewToken(envToken, target)
 	token.SetMetadata(auth.MetaKeyLogin, login)
+	token.SetMetadata(auth.MetaKeyBaseURL, defaultBaseURL)
 	err = auth.Store(repo, token)
 	require.NoError(t, err)
 
@@ -194,7 +195,7 @@ func TestPushPull(t *testing.T) {
 	exporter := &gitlabExporter{}
 	err = exporter.Init(backend, core.Configuration{
 		keyProjectID:     strconv.Itoa(projectID),
-		keyGitlabBaseUrl: "https://gitlab.com/",
+		keyGitlabBaseUrl: defaultBaseURL,
 	})
 	require.NoError(t, err)
 
@@ -222,7 +223,7 @@ func TestPushPull(t *testing.T) {
 	importer := &gitlabImporter{}
 	err = importer.Init(backend, core.Configuration{
 		keyProjectID:     strconv.Itoa(projectID),
-		keyGitlabBaseUrl: "https://gitlab.com/",
+		keyGitlabBaseUrl: defaultBaseURL,
 	})
 	require.NoError(t, err)
 
@@ -287,7 +288,7 @@ func generateRepoName() string {
 
 // create repository need a token with scope 'repo'
 func createRepository(ctx context.Context, name string, token *auth.Token) (int, error) {
-	client, err := buildClient("https://gitlab.com/", token)
+	client, err := buildClient(defaultBaseURL, token)
 	if err != nil {
 		return 0, err
 	}
@@ -307,7 +308,7 @@ func createRepository(ctx context.Context, name string, token *auth.Token) (int,
 
 // delete repository need a token with scope 'delete_repo'
 func deleteRepository(ctx context.Context, project int, token *auth.Token) error {
-	client, err := buildClient("https://gitlab.com/", token)
+	client, err := buildClient(defaultBaseURL, token)
 	if err != nil {
 		return err
 	}

bridge/gitlab/import.go 🔗

@@ -33,7 +33,11 @@ type gitlabImporter struct {
 func (gi *gitlabImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
 	gi.conf = conf
 
-	creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
+	creds, err := auth.List(repo,
+		auth.WithTarget(target),
+		auth.WithKind(auth.KindToken),
+		auth.WithMeta(auth.MetaKeyBaseURL, conf[keyGitlabBaseUrl]),
+	)
 	if err != nil {
 		return err
 	}

bridge/gitlab/import_test.go 🔗

@@ -99,14 +99,15 @@ func TestImport(t *testing.T) {
 	author.SetMetadata(metaKeyGitlabLogin, login)
 
 	token := auth.NewToken(envToken, target)
-	token.SetMetadata(metaKeyGitlabLogin, login)
+	token.SetMetadata(auth.MetaKeyLogin, login)
+	token.SetMetadata(auth.MetaKeyBaseURL, defaultBaseURL)
 	err = auth.Store(repo, token)
 	require.NoError(t, err)
 
 	importer := &gitlabImporter{}
 	err = importer.Init(backend, core.Configuration{
 		keyProjectID:     projectID,
-		keyGitlabBaseUrl: "https://gitlab.com",
+		keyGitlabBaseUrl: defaultBaseURL,
 	})
 	require.NoError(t, err)