config.go

  1package gitlab
  2
  3import (
  4	"bufio"
  5	"fmt"
  6	"net/url"
  7	"os"
  8	"regexp"
  9	"strconv"
 10	"strings"
 11
 12	"github.com/pkg/errors"
 13	"github.com/xanzy/go-gitlab"
 14
 15	"github.com/MichaelMure/git-bug/bridge/core"
 16	"github.com/MichaelMure/git-bug/repository"
 17)
 18
 19var (
 20	ErrBadProjectURL = errors.New("bad project url")
 21)
 22
 23func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
 24	if params.Project != "" {
 25		fmt.Println("warning: --project is ineffective for a gitlab bridge")
 26	}
 27	if params.Owner != "" {
 28		fmt.Println("warning: --owner is ineffective for a gitlab bridge")
 29	}
 30
 31	conf := make(core.Configuration)
 32	var err error
 33	var url string
 34	var token string
 35
 36	if (params.Token != "" || params.TokenStdin) && params.URL == "" {
 37		return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
 38	}
 39
 40	// get project url
 41	if params.URL != "" {
 42		url = params.URL
 43
 44	} else {
 45		// remote suggestions
 46		remotes, err := repo.GetRemotes()
 47		if err != nil {
 48			return nil, errors.Wrap(err, "getting remotes")
 49		}
 50
 51		// terminal prompt
 52		url, err = promptURL(remotes)
 53		if err != nil {
 54			return nil, errors.Wrap(err, "url prompt")
 55		}
 56	}
 57
 58	// get user token
 59	if params.Token != "" {
 60		token = params.Token
 61	} else if params.TokenStdin {
 62		reader := bufio.NewReader(os.Stdin)
 63		token, err = reader.ReadString('\n')
 64		if err != nil {
 65			return nil, fmt.Errorf("reading from stdin: %v", err)
 66		}
 67		token = strings.TrimSuffix(token, "\n")
 68	} else {
 69		token, err = promptToken()
 70		if err != nil {
 71			return nil, errors.Wrap(err, "token prompt")
 72		}
 73	}
 74
 75	// validate project url and get its ID
 76	id, err := validateProjectURL(url, token)
 77	if err != nil {
 78		return nil, errors.Wrap(err, "project validation")
 79	}
 80
 81	conf[keyProjectID] = strconv.Itoa(id)
 82	conf[keyToken] = token
 83	conf[core.KeyTarget] = target
 84
 85	err = g.ValidateConfig(conf)
 86	if err != nil {
 87		return nil, err
 88	}
 89
 90	return conf, nil
 91}
 92
 93func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
 94	if v, ok := conf[core.KeyTarget]; !ok {
 95		return fmt.Errorf("missing %s key", core.KeyTarget)
 96	} else if v != target {
 97		return fmt.Errorf("unexpected target name: %v", v)
 98	}
 99
100	if _, ok := conf[keyToken]; !ok {
101		return fmt.Errorf("missing %s key", keyToken)
102	}
103
104	if _, ok := conf[keyProjectID]; !ok {
105		return fmt.Errorf("missing %s key", keyProjectID)
106	}
107
108	return nil
109}
110
111func promptToken() (string, error) {
112	fmt.Println("You can generate a new token by visiting https://gitlab.com/profile/personal_access_tokens.")
113	fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
114	fmt.Println()
115	fmt.Println("'api' access scope: to be able to make api calls")
116	fmt.Println()
117
118	re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
119	if err != nil {
120		panic("regexp compile:" + err.Error())
121	}
122
123	for {
124		fmt.Print("Enter token: ")
125
126		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
127		if err != nil {
128			return "", err
129		}
130
131		token := strings.TrimRight(line, "\n")
132		if re.MatchString(token) {
133			return token, nil
134		}
135
136		fmt.Println("token format is invalid")
137	}
138}
139
140func promptURL(remotes map[string]string) (string, error) {
141	validRemotes := getValidGitlabRemoteURLs(remotes)
142	if len(validRemotes) > 0 {
143		for {
144			fmt.Println("\nDetected projects:")
145
146			// print valid remote gitlab urls
147			for i, remote := range validRemotes {
148				fmt.Printf("[%d]: %v\n", i+1, remote)
149			}
150
151			fmt.Printf("\n[0]: Another project\n\n")
152			fmt.Printf("Select option: ")
153
154			line, err := bufio.NewReader(os.Stdin).ReadString('\n')
155			if err != nil {
156				return "", err
157			}
158
159			line = strings.TrimRight(line, "\n")
160
161			index, err := strconv.Atoi(line)
162			if err != nil || index < 0 || index > len(validRemotes) {
163				fmt.Println("invalid input")
164				continue
165			}
166
167			// if user want to enter another project url break this loop
168			if index == 0 {
169				break
170			}
171
172			return validRemotes[index-1], nil
173		}
174	}
175
176	// manually enter gitlab url
177	for {
178		fmt.Print("Gitlab project URL: ")
179
180		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
181		if err != nil {
182			return "", err
183		}
184
185		url := strings.TrimRight(line, "\n")
186		if line == "" {
187			fmt.Println("URL is empty")
188			continue
189		}
190
191		return url, nil
192	}
193}
194
195func getProjectPath(projectUrl string) (string, error) {
196	cleanUrl := strings.TrimSuffix(projectUrl, ".git")
197	cleanUrl = strings.Replace(cleanUrl, "git@", "https://", 1)
198	objectUrl, err := url.Parse(cleanUrl)
199	if err != nil {
200		return "", ErrBadProjectURL
201	}
202
203	return objectUrl.Path[1:], nil
204}
205
206func getValidGitlabRemoteURLs(remotes map[string]string) []string {
207	urls := make([]string, 0, len(remotes))
208	for _, u := range remotes {
209		path, err := getProjectPath(u)
210		if err != nil {
211			continue
212		}
213
214		urls = append(urls, fmt.Sprintf("%s%s", "gitlab.com", path))
215	}
216
217	return urls
218}
219
220func validateProjectURL(url, token string) (int, error) {
221	projectPath, err := getProjectPath(url)
222	if err != nil {
223		return 0, err
224	}
225
226	client := buildClient(token)
227
228	project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
229	if err != nil {
230		return 0, err
231	}
232
233	return project.ID, nil
234}