2019-07-09 23:58:47 +03:00
|
|
|
package gitlab
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
2019-07-22 19:56:14 +03:00
|
|
|
"net/url"
|
2019-07-09 23:58:47 +03:00
|
|
|
"os"
|
2020-01-03 15:53:59 +03:00
|
|
|
"path"
|
2019-07-09 23:58:47 +03:00
|
|
|
"regexp"
|
2019-12-08 23:15:06 +03:00
|
|
|
"sort"
|
2019-07-09 23:58:47 +03:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2019-11-26 22:45:32 +03:00
|
|
|
"time"
|
2019-07-09 23:58:47 +03:00
|
|
|
|
2019-11-26 22:45:32 +03:00
|
|
|
text "github.com/MichaelMure/go-term-text"
|
2019-07-09 23:58:47 +03:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/xanzy/go-gitlab"
|
|
|
|
|
|
|
|
"github.com/MichaelMure/git-bug/bridge/core"
|
2019-12-08 23:15:06 +03:00
|
|
|
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
|
|
|
"github.com/MichaelMure/git-bug/cache"
|
2020-02-04 02:25:27 +03:00
|
|
|
"github.com/MichaelMure/git-bug/input"
|
2019-07-09 23:58:47 +03:00
|
|
|
"github.com/MichaelMure/git-bug/repository"
|
2019-11-26 22:45:32 +03:00
|
|
|
"github.com/MichaelMure/git-bug/util/colors"
|
2019-07-09 23:58:47 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrBadProjectURL = errors.New("bad project url")
|
|
|
|
)
|
|
|
|
|
2019-12-08 23:15:06 +03:00
|
|
|
func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
|
2019-07-09 23:58:47 +03:00
|
|
|
if params.Project != "" {
|
|
|
|
fmt.Println("warning: --project is ineffective for a gitlab bridge")
|
|
|
|
}
|
|
|
|
if params.Owner != "" {
|
|
|
|
fmt.Println("warning: --owner is ineffective for a gitlab bridge")
|
|
|
|
}
|
2020-02-04 02:25:27 +03:00
|
|
|
if params.Login != "" {
|
|
|
|
fmt.Println("warning: --login is ineffective for a gitlab bridge")
|
|
|
|
}
|
2019-07-09 23:58:47 +03:00
|
|
|
|
|
|
|
conf := make(core.Configuration)
|
|
|
|
var err error
|
2019-12-26 22:16:18 +03:00
|
|
|
var baseUrl string
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case params.BaseURL != "":
|
|
|
|
baseUrl = params.BaseURL
|
|
|
|
default:
|
|
|
|
baseUrl, err = promptBaseUrlOptions()
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "base url prompt")
|
|
|
|
}
|
2019-12-10 22:30:29 +03:00
|
|
|
}
|
|
|
|
|
2019-12-08 23:15:06 +03:00
|
|
|
var url string
|
|
|
|
|
2019-07-09 23:58:47 +03:00
|
|
|
// get project url
|
2019-12-08 23:15:06 +03:00
|
|
|
switch {
|
|
|
|
case params.URL != "":
|
2019-07-09 23:58:47 +03:00
|
|
|
url = params.URL
|
2019-12-08 23:15:06 +03:00
|
|
|
default:
|
2019-07-09 23:58:47 +03:00
|
|
|
// terminal prompt
|
2019-12-26 22:16:18 +03:00
|
|
|
url, err = promptURL(repo, baseUrl)
|
2019-07-09 23:58:47 +03:00
|
|
|
if err != nil {
|
2019-07-22 19:56:14 +03:00
|
|
|
return nil, errors.Wrap(err, "url prompt")
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-10 22:30:29 +03:00
|
|
|
if !strings.HasPrefix(url, params.BaseURL) {
|
|
|
|
return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, url)
|
|
|
|
}
|
|
|
|
|
2019-12-08 23:15:06 +03:00
|
|
|
var cred auth.Credential
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case params.CredPrefix != "":
|
|
|
|
cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
|
2019-11-24 16:39:02 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-12-08 23:15:06 +03:00
|
|
|
case params.TokenRaw != "":
|
2020-02-04 02:25:27 +03:00
|
|
|
token := auth.NewToken(params.TokenRaw, target)
|
|
|
|
login, err := getLoginFromToken(baseUrl, token)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
token.SetMetadata(auth.MetaKeyLogin, login)
|
|
|
|
cred = token
|
2019-12-08 23:15:06 +03:00
|
|
|
default:
|
2020-02-04 02:25:27 +03:00
|
|
|
cred, err = promptTokenOptions(repo, baseUrl)
|
2019-11-24 16:39:02 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-12-08 23:15:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
token, ok := cred.(*auth.Token)
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("the Gitlab bridge only handle token credentials")
|
2019-11-24 16:39:02 +03:00
|
|
|
}
|
|
|
|
|
2019-07-23 18:10:07 +03:00
|
|
|
// validate project url and get its ID
|
2019-12-26 22:16:18 +03:00
|
|
|
id, err := validateProjectURL(baseUrl, url, token)
|
2019-07-09 23:58:47 +03:00
|
|
|
if err != nil {
|
2019-07-22 19:56:14 +03:00
|
|
|
return nil, errors.Wrap(err, "project validation")
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
|
|
|
|
2019-11-10 19:48:13 +03:00
|
|
|
conf[core.ConfigKeyTarget] = target
|
2019-12-08 23:15:06 +03:00
|
|
|
conf[keyProjectID] = strconv.Itoa(id)
|
2019-12-26 22:16:18 +03:00
|
|
|
conf[keyGitlabBaseUrl] = baseUrl
|
2019-07-09 23:58:47 +03:00
|
|
|
|
2019-08-21 15:24:48 +03:00
|
|
|
err = g.ValidateConfig(conf)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-12-08 23:15:06 +03:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-09 23:58:47 +03:00
|
|
|
return conf, nil
|
|
|
|
}
|
|
|
|
|
2019-08-21 15:24:48 +03:00
|
|
|
func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
|
2019-11-10 19:48:13 +03:00
|
|
|
if v, ok := conf[core.ConfigKeyTarget]; !ok {
|
|
|
|
return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
|
2019-07-09 23:58:47 +03:00
|
|
|
} else if v != target {
|
|
|
|
return fmt.Errorf("unexpected target name: %v", v)
|
|
|
|
}
|
2019-12-26 22:16:18 +03:00
|
|
|
if _, ok := conf[keyGitlabBaseUrl]; !ok {
|
|
|
|
return fmt.Errorf("missing %s key", keyGitlabBaseUrl)
|
|
|
|
}
|
2019-07-12 18:31:01 +03:00
|
|
|
if _, ok := conf[keyProjectID]; !ok {
|
|
|
|
return fmt.Errorf("missing %s key", keyProjectID)
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-12-26 22:16:18 +03:00
|
|
|
func promptBaseUrlOptions() (string, error) {
|
2020-02-04 02:25:27 +03:00
|
|
|
index, err := input.PromptChoice("Gitlab base url", []string{
|
|
|
|
"https://gitlab.com",
|
|
|
|
"enter your own base url",
|
|
|
|
})
|
2019-12-26 22:16:18 +03:00
|
|
|
|
2020-02-04 02:25:27 +03:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2019-12-26 22:16:18 +03:00
|
|
|
|
2020-02-04 02:25:27 +03:00
|
|
|
if index == 0 {
|
|
|
|
return defaultBaseURL, nil
|
|
|
|
} else {
|
|
|
|
return promptBaseUrl()
|
2019-12-26 22:16:18 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func promptBaseUrl() (string, error) {
|
2020-02-04 02:25:27 +03:00
|
|
|
validator := func(name string, value string) (string, error) {
|
|
|
|
u, err := url.Parse(value)
|
2019-12-26 22:16:18 +03:00
|
|
|
if err != nil {
|
2020-02-04 02:25:27 +03:00
|
|
|
return err.Error(), nil
|
2019-12-26 22:16:18 +03:00
|
|
|
}
|
2020-02-04 02:25:27 +03:00
|
|
|
if u.Scheme == "" {
|
|
|
|
return "missing scheme", nil
|
2019-12-26 22:16:18 +03:00
|
|
|
}
|
2020-02-04 02:25:27 +03:00
|
|
|
if u.Host == "" {
|
|
|
|
return "missing host", nil
|
2019-12-26 22:16:18 +03:00
|
|
|
}
|
2020-02-04 02:25:27 +03:00
|
|
|
return "", nil
|
2019-12-26 22:16:18 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 02:25:27 +03:00
|
|
|
return input.Prompt("Base url", "url", input.Required, validator)
|
2019-12-26 22:16:18 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 02:25:27 +03:00
|
|
|
func promptTokenOptions(repo repository.RepoConfig, baseUrl string) (auth.Credential, error) {
|
2019-11-24 16:39:02 +03:00
|
|
|
for {
|
2020-01-08 00:07:25 +03:00
|
|
|
creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
|
2019-11-24 16:39:02 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-12-08 23:15:06 +03:00
|
|
|
// if we don't have existing token, fast-track to the token prompt
|
|
|
|
if len(creds) == 0 {
|
2020-02-04 02:25:27 +03:00
|
|
|
return promptToken(baseUrl)
|
2019-11-26 22:45:32 +03:00
|
|
|
}
|
|
|
|
|
2019-11-24 16:39:02 +03:00
|
|
|
fmt.Println()
|
2019-11-26 22:45:32 +03:00
|
|
|
fmt.Println("[1]: enter my token")
|
|
|
|
|
|
|
|
fmt.Println()
|
|
|
|
fmt.Println("Existing tokens for Gitlab:")
|
2019-12-08 23:15:06 +03:00
|
|
|
|
|
|
|
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),
|
|
|
|
)
|
2019-11-24 16:39:02 +03:00
|
|
|
}
|
2019-11-26 22:45:32 +03:00
|
|
|
|
|
|
|
fmt.Println()
|
2019-11-24 16:39:02 +03:00
|
|
|
fmt.Print("Select option: ")
|
|
|
|
|
|
|
|
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
|
|
fmt.Println()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-11-25 17:08:48 +03:00
|
|
|
line = strings.TrimSpace(line)
|
2019-11-24 16:39:02 +03:00
|
|
|
index, err := strconv.Atoi(line)
|
2019-12-08 23:15:06 +03:00
|
|
|
if err != nil || index < 1 || index > len(creds)+1 {
|
2019-11-24 16:39:02 +03:00
|
|
|
fmt.Println("invalid input")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
switch index {
|
|
|
|
case 1:
|
2020-02-04 02:25:27 +03:00
|
|
|
return promptToken(baseUrl)
|
2019-11-24 16:39:02 +03:00
|
|
|
default:
|
2019-12-08 23:15:06 +03:00
|
|
|
return creds[index-2], nil
|
2019-11-24 16:39:02 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-04 02:25:27 +03:00
|
|
|
func promptToken(baseUrl string) (*auth.Token, error) {
|
2020-01-03 15:53:59 +03:00
|
|
|
fmt.Printf("You can generate a new token by visiting %s.\n", path.Join(baseUrl, "profile/personal_access_tokens"))
|
2019-07-19 19:49:28 +03:00
|
|
|
fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
|
2019-07-09 23:58:47 +03:00
|
|
|
fmt.Println()
|
2019-07-19 19:49:28 +03:00
|
|
|
fmt.Println("'api' access scope: to be able to make api calls")
|
2019-07-09 23:58:47 +03:00
|
|
|
fmt.Println()
|
|
|
|
|
2020-02-04 02:25:27 +03:00
|
|
|
re, err := regexp.Compile(`^[a-zA-Z0-9\-\_]{20}$`)
|
2019-07-09 23:58:47 +03:00
|
|
|
if err != nil {
|
|
|
|
panic("regexp compile:" + err.Error())
|
|
|
|
}
|
|
|
|
|
2020-02-04 02:25:27 +03:00
|
|
|
var login string
|
2019-07-09 23:58:47 +03:00
|
|
|
|
2020-02-04 02:25:27 +03:00
|
|
|
validator := func(name string, value string) (complaint string, err error) {
|
|
|
|
if !re.MatchString(value) {
|
|
|
|
return "token has incorrect format", nil
|
|
|
|
}
|
|
|
|
login, err = getLoginFromToken(baseUrl, auth.NewToken(value, target))
|
2019-07-09 23:58:47 +03:00
|
|
|
if err != nil {
|
2020-02-04 02:25:27 +03:00
|
|
|
return fmt.Sprintf("token is invalid: %v", err), nil
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
2020-02-04 02:25:27 +03:00
|
|
|
return "", nil
|
|
|
|
}
|
2019-07-09 23:58:47 +03:00
|
|
|
|
2020-02-04 02:25:27 +03:00
|
|
|
rawToken, err := input.Prompt("Enter token", "token", input.Required, validator)
|
2020-02-09 00:08:35 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-07-09 23:58:47 +03:00
|
|
|
|
2020-02-04 02:25:27 +03:00
|
|
|
token := auth.NewToken(rawToken, target)
|
|
|
|
token.SetMetadata(auth.MetaKeyLogin, login)
|
|
|
|
|
|
|
|
return token, nil
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
|
|
|
|
2019-12-26 22:16:18 +03:00
|
|
|
func promptURL(repo repository.RepoCommon, baseUrl string) (string, error) {
|
2019-12-08 23:15:06 +03:00
|
|
|
// remote suggestions
|
|
|
|
remotes, err := repo.GetRemotes()
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrap(err, "getting remotes")
|
|
|
|
}
|
|
|
|
|
2019-12-26 22:16:18 +03:00
|
|
|
validRemotes := getValidGitlabRemoteURLs(baseUrl, remotes)
|
2019-07-09 23:58:47 +03:00
|
|
|
if len(validRemotes) > 0 {
|
|
|
|
for {
|
|
|
|
fmt.Println("\nDetected projects:")
|
|
|
|
|
|
|
|
// print valid remote gitlab urls
|
|
|
|
for i, remote := range validRemotes {
|
|
|
|
fmt.Printf("[%d]: %v\n", i+1, remote)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("\n[0]: Another project\n\n")
|
|
|
|
fmt.Printf("Select option: ")
|
|
|
|
|
|
|
|
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2019-11-25 17:08:48 +03:00
|
|
|
line = strings.TrimSpace(line)
|
2019-07-09 23:58:47 +03:00
|
|
|
|
|
|
|
index, err := strconv.Atoi(line)
|
2019-07-23 18:29:53 +03:00
|
|
|
if err != nil || index < 0 || index > len(validRemotes) {
|
2019-07-09 23:58:47 +03:00
|
|
|
fmt.Println("invalid input")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// if user want to enter another project url break this loop
|
|
|
|
if index == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
return validRemotes[index-1], nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// manually enter gitlab url
|
|
|
|
for {
|
|
|
|
fmt.Print("Gitlab project URL: ")
|
|
|
|
|
|
|
|
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2019-11-25 17:08:48 +03:00
|
|
|
url := strings.TrimSpace(line)
|
2019-11-26 22:45:32 +03:00
|
|
|
if url == "" {
|
2019-07-09 23:58:47 +03:00
|
|
|
fmt.Println("URL is empty")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
return url, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-26 22:16:18 +03:00
|
|
|
func getProjectPath(baseUrl, projectUrl string) (string, error) {
|
2019-07-22 19:56:14 +03:00
|
|
|
cleanUrl := strings.TrimSuffix(projectUrl, ".git")
|
2019-07-19 19:49:28 +03:00
|
|
|
cleanUrl = strings.Replace(cleanUrl, "git@", "https://", 1)
|
2019-07-22 19:56:14 +03:00
|
|
|
objectUrl, err := url.Parse(cleanUrl)
|
2019-07-09 23:58:47 +03:00
|
|
|
if err != nil {
|
2019-07-22 19:56:14 +03:00
|
|
|
return "", ErrBadProjectURL
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
|
|
|
|
2019-12-26 22:16:18 +03:00
|
|
|
objectBaseUrl, err := url.Parse(baseUrl)
|
|
|
|
if err != nil {
|
|
|
|
return "", ErrBadProjectURL
|
|
|
|
}
|
|
|
|
|
|
|
|
if objectUrl.Hostname() != objectBaseUrl.Hostname() {
|
|
|
|
return "", fmt.Errorf("base url and project url hostnames doesn't match")
|
|
|
|
}
|
2019-07-10 01:41:43 +03:00
|
|
|
return objectUrl.Path[1:], nil
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
|
|
|
|
2019-12-26 22:16:18 +03:00
|
|
|
func getValidGitlabRemoteURLs(baseUrl string, remotes map[string]string) []string {
|
2019-07-09 23:58:47 +03:00
|
|
|
urls := make([]string, 0, len(remotes))
|
|
|
|
for _, u := range remotes {
|
2019-12-26 22:16:18 +03:00
|
|
|
path, err := getProjectPath(baseUrl, u)
|
2019-07-09 23:58:47 +03:00
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-12-26 22:16:18 +03:00
|
|
|
urls = append(urls, fmt.Sprintf("%s/%s", baseUrl, path))
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return urls
|
|
|
|
}
|
|
|
|
|
2019-12-26 22:16:18 +03:00
|
|
|
func validateProjectURL(baseUrl, url string, token *auth.Token) (int, error) {
|
|
|
|
projectPath, err := getProjectPath(baseUrl, url)
|
2019-07-09 23:58:47 +03:00
|
|
|
if err != nil {
|
2019-07-23 18:29:53 +03:00
|
|
|
return 0, err
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
|
|
|
|
2019-12-26 22:16:18 +03:00
|
|
|
client, err := buildClient(baseUrl, token)
|
2019-12-10 22:30:29 +03:00
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
2019-07-23 18:29:53 +03:00
|
|
|
|
2019-07-10 01:41:43 +03:00
|
|
|
project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
|
2019-07-09 23:58:47 +03:00
|
|
|
if err != nil {
|
2020-02-04 02:25:27 +03:00
|
|
|
return 0, errors.Wrap(err, "wrong token scope ou non-existent project")
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
|
|
|
|
2019-07-23 18:29:53 +03:00
|
|
|
return project.ID, nil
|
2019-07-09 23:58:47 +03:00
|
|
|
}
|
2020-02-04 02:25:27 +03:00
|
|
|
|
|
|
|
func getLoginFromToken(baseUrl string, token *auth.Token) (string, error) {
|
|
|
|
client, err := buildClient(baseUrl, token)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
user, _, err := client.Users.CurrentUser()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
if user.Username == "" {
|
|
|
|
return "", fmt.Errorf("gitlab say username is empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
return user.Username, nil
|
|
|
|
}
|