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 = promptToken()
 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
125func promptToken() (string, error) {
126	fmt.Println("You can generate a new token by visiting https://gitlab.com/settings/tokens.")
127	fmt.Println("Choose 'Generate new token' and set the necessary access scope for your repository.")
128	fmt.Println()
129	fmt.Println("'api' scope access : access scope: to be able to make api calls")
130	fmt.Println()
131
132	re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
133	if err != nil {
134		panic("regexp compile:" + err.Error())
135	}
136
137	for {
138		fmt.Print("Enter token: ")
139
140		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
141		if err != nil {
142			return "", err
143		}
144
145		token := strings.TrimRight(line, "\n")
146		if re.MatchString(token) {
147			return token, nil
148		}
149
150		fmt.Println("token is invalid")
151	}
152}
153
154func promptURL(remotes map[string]string) (string, error) {
155	validRemotes := getValidGitlabRemoteURLs(remotes)
156	if len(validRemotes) > 0 {
157		for {
158			fmt.Println("\nDetected projects:")
159
160			// print valid remote gitlab urls
161			for i, remote := range validRemotes {
162				fmt.Printf("[%d]: %v\n", i+1, remote)
163			}
164
165			fmt.Printf("\n[0]: Another project\n\n")
166			fmt.Printf("Select option: ")
167
168			line, err := bufio.NewReader(os.Stdin).ReadString('\n')
169			if err != nil {
170				return "", err
171			}
172
173			line = strings.TrimRight(line, "\n")
174
175			index, err := strconv.Atoi(line)
176			if err != nil || (index < 0 && index >= len(validRemotes)) {
177				fmt.Println("invalid input")
178				continue
179			}
180
181			// if user want to enter another project url break this loop
182			if index == 0 {
183				break
184			}
185
186			return validRemotes[index-1], nil
187		}
188	}
189
190	// manually enter gitlab url
191	for {
192		fmt.Print("Gitlab project URL: ")
193
194		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
195		if err != nil {
196			return "", err
197		}
198
199		url := strings.TrimRight(line, "\n")
200		if line == "" {
201			fmt.Println("URL is empty")
202			continue
203		}
204
205		return url, nil
206	}
207}
208
209func getProjectPath(url string) (string, error) {
210
211	cleanUrl := strings.TrimSuffix(url, ".git")
212	objectUrl, err := neturl.Parse(cleanUrl)
213	if err != nil {
214		return "", nil
215	}
216
217	return objectUrl.Path[1:], nil
218}
219
220func getValidGitlabRemoteURLs(remotes map[string]string) []string {
221	urls := make([]string, 0, len(remotes))
222	for _, u := range remotes {
223		path, err := getProjectPath(u)
224		if err != nil {
225			continue
226		}
227
228		urls = append(urls, fmt.Sprintf("%s%s", "gitlab.com", path))
229	}
230
231	return urls
232}
233
234func validateUsername(username string) (bool, int, error) {
235	// no need for a token for this action
236	client := buildClient("")
237
238	users, _, err := client.Users.ListUsers(&gitlab.ListUsersOptions{Username: &username})
239	if err != nil {
240		return false, 0, err
241	}
242
243	if len(users) == 0 {
244		return false, 0, fmt.Errorf("username not found")
245	} else if len(users) > 1 {
246		return false, 0, fmt.Errorf("found multiple matches")
247	}
248
249	if users[0].Username == username {
250		return true, users[0].ID, nil
251	}
252
253	return false, 0, nil
254}
255
256func validateProjectURL(url, token string) (bool, int, error) {
257	client := buildClient(token)
258
259	projectPath, err := getProjectPath(url)
260	if err != nil {
261		return false, 0, err
262	}
263
264	project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
265	if err != nil {
266		return false, 0, err
267	}
268
269	return true, project.ID, nil
270}