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/repository"
23 "github.com/MichaelMure/git-bug/util/colors"
24)
25
26var (
27 ErrBadProjectURL = errors.New("bad project url")
28)
29
30func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
31 if params.Project != "" {
32 fmt.Println("warning: --project is ineffective for a gitlab bridge")
33 }
34 if params.Owner != "" {
35 fmt.Println("warning: --owner is ineffective for a gitlab bridge")
36 }
37
38 conf := make(core.Configuration)
39 var err error
40
41 if (params.CredPrefix != "" || params.TokenRaw != "") && params.URL == "" {
42 return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
43 }
44
45 var url string
46
47 // get project url
48 switch {
49 case params.URL != "":
50 url = params.URL
51 default:
52 // terminal prompt
53 url, err = promptURL(repo)
54 if err != nil {
55 return nil, errors.Wrap(err, "url prompt")
56 }
57 }
58
59 user, err := repo.GetUserIdentity()
60 if err != nil {
61 return nil, err
62 }
63
64 var cred auth.Credential
65
66 switch {
67 case params.CredPrefix != "":
68 cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
69 if err != nil {
70 return nil, err
71 }
72 if cred.UserId() != user.Id() {
73 return nil, fmt.Errorf("selected credential don't match the user")
74 }
75 case params.TokenRaw != "":
76 cred = auth.NewToken(user.Id(), params.TokenRaw, target)
77 default:
78 cred, err = promptTokenOptions(repo, user.Id())
79 if err != nil {
80 return nil, err
81 }
82 }
83
84 token, ok := cred.(*auth.Token)
85 if !ok {
86 return nil, fmt.Errorf("the Gitlab bridge only handle token credentials")
87 }
88
89 // validate project url and get its ID
90 id, err := validateProjectURL(url, token)
91 if err != nil {
92 return nil, errors.Wrap(err, "project validation")
93 }
94
95 conf[core.ConfigKeyTarget] = target
96 conf[keyProjectID] = strconv.Itoa(id)
97
98 err = g.ValidateConfig(conf)
99 if err != nil {
100 return nil, err
101 }
102
103 // don't forget to store the now known valid token
104 if !auth.IdExist(repo, cred.ID()) {
105 err = auth.Store(repo, cred)
106 if err != nil {
107 return nil, err
108 }
109 }
110
111 return conf, nil
112}
113
114func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
115 if v, ok := conf[core.ConfigKeyTarget]; !ok {
116 return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
117 } else if v != target {
118 return fmt.Errorf("unexpected target name: %v", v)
119 }
120
121 if _, ok := conf[keyProjectID]; !ok {
122 return fmt.Errorf("missing %s key", keyProjectID)
123 }
124
125 return nil
126}
127
128func promptTokenOptions(repo repository.RepoConfig, userId entity.Id) (auth.Credential, error) {
129 for {
130 creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target), auth.WithKind(auth.KindToken))
131 if err != nil {
132 return nil, err
133 }
134
135 // if we don't have existing token, fast-track to the token prompt
136 if len(creds) == 0 {
137 value, err := promptToken()
138 if err != nil {
139 return nil, err
140 }
141 return auth.NewToken(userId, value, target), nil
142 }
143
144 fmt.Println()
145 fmt.Println("[1]: enter my token")
146
147 fmt.Println()
148 fmt.Println("Existing tokens for Gitlab:")
149
150 sort.Sort(auth.ById(creds))
151 for i, cred := range creds {
152 token := cred.(*auth.Token)
153 fmt.Printf("[%d]: %s => %s (%s)\n",
154 i+2,
155 colors.Cyan(token.ID().Human()),
156 colors.Red(text.TruncateMax(token.Value, 10)),
157 token.CreateTime().Format(time.RFC822),
158 )
159 }
160
161 fmt.Println()
162 fmt.Print("Select option: ")
163
164 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
165 fmt.Println()
166 if err != nil {
167 return nil, err
168 }
169
170 line = strings.TrimSpace(line)
171 index, err := strconv.Atoi(line)
172 if err != nil || index < 1 || index > len(creds)+1 {
173 fmt.Println("invalid input")
174 continue
175 }
176
177 switch index {
178 case 1:
179 value, err := promptToken()
180 if err != nil {
181 return nil, err
182 }
183 return auth.NewToken(userId, value, target), nil
184 default:
185 return creds[index-2], nil
186 }
187 }
188}
189
190func promptToken() (string, error) {
191 fmt.Println("You can generate a new token by visiting https://gitlab.com/profile/personal_access_tokens.")
192 fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
193 fmt.Println()
194 fmt.Println("'api' access scope: to be able to make api calls")
195 fmt.Println()
196
197 re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
198 if err != nil {
199 panic("regexp compile:" + err.Error())
200 }
201
202 for {
203 fmt.Print("Enter token: ")
204
205 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
206 if err != nil {
207 return "", err
208 }
209
210 token := strings.TrimSpace(line)
211 if re.MatchString(token) {
212 return token, nil
213 }
214
215 fmt.Println("token format is invalid")
216 }
217}
218
219func promptURL(repo repository.RepoCommon) (string, error) {
220 // remote suggestions
221 remotes, err := repo.GetRemotes()
222 if err != nil {
223 return "", errors.Wrap(err, "getting remotes")
224 }
225
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 string, token *auth.Token) (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}