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/repository"
17)
18
19var (
20 ErrBadProjectURL = errors.New("bad project url")
21)
22
23func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
24 if params.Project != "" {
25 fmt.Println("warning: --project is ineffective for a gitlab bridge")
26 }
27 if params.Owner != "" {
28 fmt.Println("warning: --owner is ineffective for a gitlab bridge")
29 }
30
31 conf := make(core.Configuration)
32 var err error
33 var url string
34 var token string
35
36 if (params.Token != "" || params.TokenStdin) && params.URL == "" {
37 return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
38 }
39
40 // get project url
41 if params.URL != "" {
42 url = params.URL
43
44 } else {
45 // remote suggestions
46 remotes, err := repo.GetRemotes()
47 if err != nil {
48 return nil, errors.Wrap(err, "getting remotes")
49 }
50
51 // terminal prompt
52 url, err = promptURL(remotes)
53 if err != nil {
54 return nil, errors.Wrap(err, "url prompt")
55 }
56 }
57
58 // get user token
59 if params.Token != "" {
60 token = params.Token
61 } else if params.TokenStdin {
62 reader := bufio.NewReader(os.Stdin)
63 token, err = reader.ReadString('\n')
64 if err != nil {
65 return nil, fmt.Errorf("reading from stdin: %v", err)
66 }
67 token = strings.TrimSuffix(token, "\n")
68 } else {
69 token, err = promptToken()
70 if err != nil {
71 return nil, errors.Wrap(err, "token prompt")
72 }
73 }
74
75 // validate project url and get its ID
76 id, err := validateProjectURL(url, token)
77 if err != nil {
78 return nil, errors.Wrap(err, "project validation")
79 }
80
81 conf[keyProjectID] = strconv.Itoa(id)
82 conf[keyToken] = token
83 conf[core.ConfigKeyTarget] = target
84
85 err = g.ValidateConfig(conf)
86 if err != nil {
87 return nil, err
88 }
89
90 return conf, nil
91}
92
93func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
94 if v, ok := conf[core.ConfigKeyTarget]; !ok {
95 return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
96 } else if v != target {
97 return fmt.Errorf("unexpected target name: %v", v)
98 }
99
100 if _, ok := conf[keyToken]; !ok {
101 return fmt.Errorf("missing %s key", keyToken)
102 }
103
104 if _, ok := conf[keyProjectID]; !ok {
105 return fmt.Errorf("missing %s key", keyProjectID)
106 }
107
108 return nil
109}
110
111func promptToken() (string, error) {
112 fmt.Println("You can generate a new token by visiting https://gitlab.com/profile/personal_access_tokens.")
113 fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
114 fmt.Println()
115 fmt.Println("'api' access scope: to be able to make api calls")
116 fmt.Println()
117
118 re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
119 if err != nil {
120 panic("regexp compile:" + err.Error())
121 }
122
123 for {
124 fmt.Print("Enter token: ")
125
126 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
127 if err != nil {
128 return "", err
129 }
130
131 token := strings.TrimRight(line, "\n")
132 if re.MatchString(token) {
133 return token, nil
134 }
135
136 fmt.Println("token format is invalid")
137 }
138}
139
140func promptURL(remotes map[string]string) (string, error) {
141 validRemotes := getValidGitlabRemoteURLs(remotes)
142 if len(validRemotes) > 0 {
143 for {
144 fmt.Println("\nDetected projects:")
145
146 // print valid remote gitlab urls
147 for i, remote := range validRemotes {
148 fmt.Printf("[%d]: %v\n", i+1, remote)
149 }
150
151 fmt.Printf("\n[0]: Another project\n\n")
152 fmt.Printf("Select option: ")
153
154 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
155 if err != nil {
156 return "", err
157 }
158
159 line = strings.TrimRight(line, "\n")
160
161 index, err := strconv.Atoi(line)
162 if err != nil || index < 0 || index > len(validRemotes) {
163 fmt.Println("invalid input")
164 continue
165 }
166
167 // if user want to enter another project url break this loop
168 if index == 0 {
169 break
170 }
171
172 return validRemotes[index-1], nil
173 }
174 }
175
176 // manually enter gitlab url
177 for {
178 fmt.Print("Gitlab project URL: ")
179
180 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
181 if err != nil {
182 return "", err
183 }
184
185 url := strings.TrimRight(line, "\n")
186 if line == "" {
187 fmt.Println("URL is empty")
188 continue
189 }
190
191 return url, nil
192 }
193}
194
195func getProjectPath(projectUrl string) (string, error) {
196 cleanUrl := strings.TrimSuffix(projectUrl, ".git")
197 cleanUrl = strings.Replace(cleanUrl, "git@", "https://", 1)
198 objectUrl, err := url.Parse(cleanUrl)
199 if err != nil {
200 return "", ErrBadProjectURL
201 }
202
203 return objectUrl.Path[1:], nil
204}
205
206func getValidGitlabRemoteURLs(remotes map[string]string) []string {
207 urls := make([]string, 0, len(remotes))
208 for _, u := range remotes {
209 path, err := getProjectPath(u)
210 if err != nil {
211 continue
212 }
213
214 urls = append(urls, fmt.Sprintf("%s%s", "gitlab.com", path))
215 }
216
217 return urls
218}
219
220func validateProjectURL(url, token string) (int, error) {
221 projectPath, err := getProjectPath(url)
222 if err != nil {
223 return 0, err
224 }
225
226 client := buildClient(token)
227
228 project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
229 if err != nil {
230 return 0, err
231 }
232
233 return project.ID, nil
234}