config.go

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