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