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}