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