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/entity"
 17	"github.com/MichaelMure/git-bug/repository"
 18)
 19
 20var (
 21	ErrBadProjectURL = errors.New("bad project url")
 22)
 23
 24func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
 25	if params.Project != "" {
 26		fmt.Println("warning: --project is ineffective for a gitlab bridge")
 27	}
 28	if params.Owner != "" {
 29		fmt.Println("warning: --owner is ineffective for a gitlab bridge")
 30	}
 31
 32	conf := make(core.Configuration)
 33	var err error
 34	var url string
 35	var token string
 36	var tokenId entity.Id
 37	var tokenObj *core.Token
 38
 39	if (params.Token != "" || params.TokenStdin) && params.URL == "" {
 40		return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
 41	}
 42
 43	// get project url
 44	if params.URL != "" {
 45		url = params.URL
 46
 47	} else {
 48		// remote suggestions
 49		remotes, err := repo.GetRemotes()
 50		if err != nil {
 51			return nil, errors.Wrap(err, "getting remotes")
 52		}
 53
 54		// terminal prompt
 55		url, err = promptURL(remotes)
 56		if err != nil {
 57			return nil, errors.Wrap(err, "url prompt")
 58		}
 59	}
 60
 61	// get user token
 62	if params.Token != "" {
 63		token = params.Token
 64	} else if params.TokenStdin {
 65		reader := bufio.NewReader(os.Stdin)
 66		token, err = reader.ReadString('\n')
 67		if err != nil {
 68			return nil, fmt.Errorf("reading from stdin: %v", err)
 69		}
 70		token = strings.TrimSpace(token)
 71	} else if params.TokenId != "" {
 72		tokenId = entity.Id(params.TokenId)
 73	} else {
 74		tokenObj, err = promptTokenOptions(repo)
 75		if err != nil {
 76			return nil, errors.Wrap(err, "token prompt")
 77		}
 78	}
 79
 80	if token != "" {
 81		tokenObj, err = core.LoadOrCreateToken(repo, target, token)
 82		if err != nil {
 83			return nil, err
 84		}
 85	} else if tokenId != "" {
 86		tokenObj, err = core.LoadToken(repo, entity.Id(tokenId))
 87		if err != nil {
 88			return nil, err
 89		}
 90		if tokenObj.Target != target {
 91			return nil, fmt.Errorf("token target is incompatible %s", tokenObj.Target)
 92		}
 93	}
 94
 95	// validate project url and get its ID
 96	id, err := validateProjectURL(url, tokenObj.Value)
 97	if err != nil {
 98		return nil, errors.Wrap(err, "project validation")
 99	}
100
101	conf[keyProjectID] = strconv.Itoa(id)
102	conf[core.ConfigKeyTokenId] = tokenObj.ID().String()
103	conf[core.ConfigKeyTarget] = target
104
105	err = g.ValidateConfig(conf)
106	if err != nil {
107		return nil, err
108	}
109
110	return conf, nil
111}
112
113func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
114	if v, ok := conf[core.ConfigKeyTarget]; !ok {
115		return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
116	} else if v != target {
117		return fmt.Errorf("unexpected target name: %v", v)
118	}
119
120	if _, ok := conf[keyToken]; !ok {
121		return fmt.Errorf("missing %s key", keyToken)
122	}
123
124	if _, ok := conf[keyProjectID]; !ok {
125		return fmt.Errorf("missing %s key", keyProjectID)
126	}
127
128	return nil
129}
130
131func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) {
132	for {
133		tokens, err := core.LoadTokensWithTarget(repo, target)
134		if err != nil {
135			return nil, err
136		}
137
138		fmt.Println()
139		fmt.Println("[1]: user provided token")
140
141		if len(tokens) > 0 {
142			fmt.Println("known tokens for Gitlab:")
143			for i, token := range tokens {
144				if token.Target == target {
145					fmt.Printf("[%d]: %s\n", i+2, token.ID())
146				}
147			}
148		}
149		fmt.Print("Select option: ")
150
151		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
152		fmt.Println()
153		if err != nil {
154			return nil, err
155		}
156
157		line = strings.TrimSpace(line)
158		index, err := strconv.Atoi(line)
159		if err != nil || index < 1 || index > len(tokens)+1 {
160			fmt.Println("invalid input")
161			continue
162		}
163
164		var token string
165		switch index {
166		case 1:
167			token, err = promptToken()
168			if err != nil {
169				return nil, err
170			}
171		default:
172			return tokens[index-2], nil
173		}
174
175		return core.LoadOrCreateToken(repo, target, token)
176	}
177}
178
179func promptToken() (string, error) {
180	fmt.Println("You can generate a new token by visiting https://gitlab.com/profile/personal_access_tokens.")
181	fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
182	fmt.Println()
183	fmt.Println("'api' access scope: to be able to make api calls")
184	fmt.Println()
185
186	re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
187	if err != nil {
188		panic("regexp compile:" + err.Error())
189	}
190
191	for {
192		fmt.Print("Enter token: ")
193
194		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
195		if err != nil {
196			return "", err
197		}
198
199		token := strings.TrimSpace(line)
200		if re.MatchString(token) {
201			return token, nil
202		}
203
204		fmt.Println("token format is invalid")
205	}
206}
207
208func promptURL(remotes map[string]string) (string, error) {
209	validRemotes := getValidGitlabRemoteURLs(remotes)
210	if len(validRemotes) > 0 {
211		for {
212			fmt.Println("\nDetected projects:")
213
214			// print valid remote gitlab urls
215			for i, remote := range validRemotes {
216				fmt.Printf("[%d]: %v\n", i+1, remote)
217			}
218
219			fmt.Printf("\n[0]: Another project\n\n")
220			fmt.Printf("Select option: ")
221
222			line, err := bufio.NewReader(os.Stdin).ReadString('\n')
223			if err != nil {
224				return "", err
225			}
226
227			line = strings.TrimSpace(line)
228
229			index, err := strconv.Atoi(line)
230			if err != nil || index < 0 || index > len(validRemotes) {
231				fmt.Println("invalid input")
232				continue
233			}
234
235			// if user want to enter another project url break this loop
236			if index == 0 {
237				break
238			}
239
240			return validRemotes[index-1], nil
241		}
242	}
243
244	// manually enter gitlab url
245	for {
246		fmt.Print("Gitlab project URL: ")
247
248		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
249		if err != nil {
250			return "", err
251		}
252
253		url := strings.TrimSpace(line)
254		if line == "" {
255			fmt.Println("URL is empty")
256			continue
257		}
258
259		return url, nil
260	}
261}
262
263func getProjectPath(projectUrl string) (string, error) {
264	cleanUrl := strings.TrimSuffix(projectUrl, ".git")
265	cleanUrl = strings.Replace(cleanUrl, "git@", "https://", 1)
266	objectUrl, err := url.Parse(cleanUrl)
267	if err != nil {
268		return "", ErrBadProjectURL
269	}
270
271	return objectUrl.Path[1:], nil
272}
273
274func getValidGitlabRemoteURLs(remotes map[string]string) []string {
275	urls := make([]string, 0, len(remotes))
276	for _, u := range remotes {
277		path, err := getProjectPath(u)
278		if err != nil {
279			continue
280		}
281
282		urls = append(urls, fmt.Sprintf("%s%s", "gitlab.com", path))
283	}
284
285	return urls
286}
287
288func validateProjectURL(url, token string) (int, error) {
289	projectPath, err := getProjectPath(url)
290	if err != nil {
291		return 0, err
292	}
293
294	client := buildClient(token)
295
296	project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
297	if err != nil {
298		return 0, err
299	}
300
301	return project.ID, nil
302}