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