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