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