github.com/lelandbatey/lab@v0.12.1-0.20180712064405-55bfd303a5f0/internal/gitlab/gitlab.go (about) 1 // Package gitlab is an internal wrapper for the go-gitlab package 2 // 3 // Most functions serve to expose debug logging if set and accept a project 4 // name string over an ID 5 package gitlab 6 7 import ( 8 "fmt" 9 "io" 10 "io/ioutil" 11 "log" 12 "net/http" 13 "os" 14 "path/filepath" 15 "strings" 16 17 "github.com/pkg/errors" 18 "github.com/xanzy/go-gitlab" 19 "github.com/zaquestion/lab/internal/git" 20 ) 21 22 var ( 23 ErrProjectNotFound = errors.New("gitlab project not found") 24 ) 25 26 var ( 27 lab *gitlab.Client 28 host string 29 user string 30 ) 31 32 // Host exposes the GitLab scheme://hostname used to interact with the API 33 func Host() string { 34 return host 35 } 36 37 // User exposes the configured GitLab user 38 func User() string { 39 return user 40 } 41 42 // Init initializes a gitlab client for use throughout lab. 43 func Init(_host, _user, _token string) { 44 if len(_host) > 0 && _host[len(_host)-1 : len(_host)][0] == '/' { 45 _host = _host[0 : len(_host)-1] 46 } 47 host = _host 48 user = _user 49 lab = gitlab.NewClient(nil, _token) 50 lab.SetBaseURL(host + "/api/v4") 51 } 52 53 // Defines filepath for default GitLab templates 54 const ( 55 TmplMR = "merge_request_templates/default.md" 56 TmplIssue = "issue_templates/default.md" 57 ) 58 59 // LoadGitLabTmpl loads gitlab templates for use in creating Issues and MRs 60 // 61 // https://gitlab.com/help/user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests 62 func LoadGitLabTmpl(tmplName string) string { 63 wd, err := git.WorkingDir() 64 if err != nil { 65 log.Fatal(err) 66 } 67 68 tmplFile := filepath.Join(wd, ".gitlab", tmplName) 69 f, err := os.Open(tmplFile) 70 if os.IsNotExist(err) { 71 return "" 72 } else if err != nil { 73 log.Fatal(err) 74 } 75 76 tmpl, err := ioutil.ReadAll(f) 77 if err != nil { 78 log.Fatal(err) 79 } 80 81 return strings.TrimSpace(string(tmpl)) 82 } 83 84 var ( 85 localProjects map[string]*gitlab.Project = make(map[string]*gitlab.Project) 86 ) 87 88 // GetProject looks up a Gitlab project by ID. 89 func GetProject(projectID int) (*gitlab.Project, error) { 90 target, resp, err := lab.Projects.GetProject(projectID) 91 if resp != nil && resp.StatusCode == http.StatusNotFound { 92 return nil, ErrProjectNotFound 93 } 94 if err != nil { 95 return nil, err 96 } 97 return target, nil 98 } 99 100 // FindProject looks up the Gitlab project. If the namespace is not provided in 101 // the project string it will search for projects in the users namespace 102 func FindProject(project string) (*gitlab.Project, error) { 103 if target, ok := localProjects[project]; ok { 104 return target, nil 105 } 106 107 search := project 108 // Assuming that a "/" in the project means its owned by an org 109 if !strings.Contains(project, "/") { 110 search = user + "/" + project 111 } 112 113 target, resp, err := lab.Projects.GetProject(search) 114 if resp != nil && resp.StatusCode == http.StatusNotFound { 115 return nil, ErrProjectNotFound 116 } 117 if err != nil { 118 return nil, err 119 } 120 // fwiw, I feel bad about this 121 localProjects[project] = target 122 123 return target, nil 124 } 125 126 // Fork creates a user fork of a GitLab project 127 func Fork(project string) (string, error) { 128 if !strings.Contains(project, "/") { 129 return "", errors.New("remote must include namespace") 130 } 131 parts := strings.Split(project, "/") 132 133 // See if a fork already exists 134 target, err := FindProject(parts[1]) 135 if err == nil { 136 return target.SSHURLToRepo, nil 137 } else if err != nil && err != ErrProjectNotFound { 138 return "", err 139 } 140 141 target, err = FindProject(project) 142 if err != nil { 143 return "", err 144 } 145 146 fork, _, err := lab.Projects.ForkProject(target.ID) 147 if err != nil { 148 return "", err 149 } 150 151 return fork.SSHURLToRepo, nil 152 } 153 154 // MRCreate opens a merge request on GitLab 155 func MRCreate(project string, opts *gitlab.CreateMergeRequestOptions) (string, error) { 156 p, err := FindProject(project) 157 if err != nil { 158 return "", err 159 } 160 161 mr, _, err := lab.MergeRequests.CreateMergeRequest(p.ID, opts) 162 if err != nil { 163 return "", err 164 } 165 return mr.WebURL, nil 166 } 167 168 // MRGet retrieves the merge request from GitLab project 169 func MRGet(project string, mrNum int) (*gitlab.MergeRequest, error) { 170 p, err := FindProject(project) 171 if err != nil { 172 return nil, err 173 } 174 175 mr, _, err := lab.MergeRequests.GetMergeRequest(p.ID, mrNum) 176 if err != nil { 177 return nil, err 178 } 179 180 return mr, nil 181 } 182 183 // MRList lists the MRs on a GitLab project 184 func MRList(project string, opts *gitlab.ListProjectMergeRequestsOptions) ([]*gitlab.MergeRequest, error) { 185 p, err := FindProject(project) 186 if err != nil { 187 return nil, err 188 } 189 190 list, _, err := lab.MergeRequests.ListProjectMergeRequests(p.ID, opts) 191 if err != nil { 192 return nil, err 193 } 194 return list, nil 195 } 196 197 // MRClose closes an mr on a GitLab project 198 func MRClose(pid interface{}, id int) error { 199 mr, _, err := lab.MergeRequests.GetMergeRequest(pid, id) 200 if err != nil { 201 return err 202 } 203 if mr.State == "closed" { 204 return fmt.Errorf("mr already closed") 205 } 206 _, _, err = lab.MergeRequests.UpdateMergeRequest(pid, int(id), &gitlab.UpdateMergeRequestOptions{ 207 StateEvent: gitlab.String("close"), 208 }) 209 if err != nil { 210 return err 211 } 212 return nil 213 } 214 215 // MRMerge merges an mr on a GitLab project 216 func MRMerge(pid interface{}, id int) error { 217 _, _, err := lab.MergeRequests.AcceptMergeRequest(pid, int(id), &gitlab.AcceptMergeRequestOptions{ 218 MergeWhenPipelineSucceeds: gitlab.Bool(true), 219 }) 220 if err != nil { 221 return err 222 } 223 return nil 224 } 225 226 // IssueCreate opens a new issue on a GitLab Project 227 func IssueCreate(project string, opts *gitlab.CreateIssueOptions) (string, error) { 228 p, err := FindProject(project) 229 if err != nil { 230 return "", err 231 } 232 233 mr, _, err := lab.Issues.CreateIssue(p.ID, opts) 234 if err != nil { 235 return "", err 236 } 237 return mr.WebURL, nil 238 } 239 240 // IssueGet retrieves the issue information from a GitLab project 241 func IssueGet(project string, issueNum int) (*gitlab.Issue, error) { 242 p, err := FindProject(project) 243 if err != nil { 244 return nil, err 245 } 246 247 issue, _, err := lab.Issues.GetIssue(p.ID, issueNum) 248 if err != nil { 249 return nil, err 250 } 251 252 return issue, nil 253 } 254 255 // IssueList gets a list of issues on a GitLab Project 256 func IssueList(project string, opts *gitlab.ListProjectIssuesOptions) ([]*gitlab.Issue, error) { 257 p, err := FindProject(project) 258 if err != nil { 259 return nil, err 260 } 261 262 list, _, err := lab.Issues.ListProjectIssues(p.ID, opts) 263 if err != nil { 264 return nil, err 265 } 266 return list, nil 267 } 268 269 // IssueClose closes an issue on a GitLab project 270 func IssueClose(pid interface{}, id int) error { 271 _, err := lab.Issues.DeleteIssue(pid, int(id)) 272 if err != nil { 273 return err 274 } 275 return nil 276 } 277 278 // BranchPushed checks if a branch exists on a GitLab project 279 func BranchPushed(pid interface{}, branch string) bool { 280 b, _, err := lab.Branches.GetBranch(pid, branch) 281 if err != nil { 282 return false 283 } 284 return b != nil 285 } 286 287 // ProjectSnippetCreate creates a snippet in a project 288 func ProjectSnippetCreate(pid interface{}, opts *gitlab.CreateProjectSnippetOptions) (*gitlab.Snippet, error) { 289 snip, _, err := lab.ProjectSnippets.CreateSnippet(pid, opts) 290 if err != nil { 291 return nil, err 292 } 293 294 return snip, nil 295 } 296 297 // ProjectSnippetDelete deletes a project snippet 298 func ProjectSnippetDelete(pid interface{}, id int) error { 299 _, err := lab.ProjectSnippets.DeleteSnippet(pid, id) 300 return err 301 } 302 303 // ProjectSnippetList lists snippets on a project 304 func ProjectSnippetList(pid interface{}, opts *gitlab.ListProjectSnippetsOptions) ([]*gitlab.Snippet, error) { 305 snips, _, err := lab.ProjectSnippets.ListSnippets(pid, opts) 306 if err != nil { 307 return nil, err 308 } 309 return snips, nil 310 } 311 312 // SnippetCreate creates a personal snippet 313 func SnippetCreate(opts *gitlab.CreateSnippetOptions) (*gitlab.Snippet, error) { 314 snip, _, err := lab.Snippets.CreateSnippet(opts) 315 if err != nil { 316 return nil, err 317 } 318 319 return snip, nil 320 } 321 322 // SnippetDelete deletes a personal snippet 323 func SnippetDelete(id int) error { 324 _, err := lab.Snippets.DeleteSnippet(id) 325 return err 326 } 327 328 // SnippetList lists snippets on a project 329 func SnippetList(opts *gitlab.ListSnippetsOptions) ([]*gitlab.Snippet, error) { 330 snips, _, err := lab.Snippets.ListSnippets(opts) 331 if err != nil { 332 return nil, err 333 } 334 return snips, nil 335 } 336 337 // Lint validates .gitlab-ci.yml contents 338 func Lint(content string) (bool, error) { 339 lint, _, err := lab.Validate.Lint(content) 340 if err != nil { 341 return false, err 342 } 343 if len(lint.Errors) > 0 { 344 return false, errors.New(strings.Join(lint.Errors, " - ")) 345 } 346 return lint.Status == "valid", nil 347 } 348 349 // ProjectCreate creates a new project on GitLab 350 func ProjectCreate(opts *gitlab.CreateProjectOptions) (*gitlab.Project, error) { 351 p, _, err := lab.Projects.CreateProject(opts) 352 if err != nil { 353 return nil, err 354 } 355 return p, nil 356 } 357 358 // ProjectDelete creates a new project on GitLab 359 func ProjectDelete(pid interface{}) error { 360 _, err := lab.Projects.DeleteProject(pid) 361 if err != nil { 362 return err 363 } 364 return nil 365 } 366 367 // ProjectList gets a list of projects on GitLab 368 func ProjectList(opts *gitlab.ListProjectsOptions) ([]*gitlab.Project, error) { 369 list, _, err := lab.Projects.ListProjects(opts) 370 if err != nil { 371 return nil, err 372 } 373 return list, nil 374 } 375 376 // CIJobs returns a list of jobs in a pipeline for a given sha. The jobs are 377 // returned sorted by their CreatedAt time 378 func CIJobs(pid interface{}, branch string) ([]*gitlab.Job, error) { 379 pipelines, _, err := lab.Pipelines.ListProjectPipelines(pid, &gitlab.ListProjectPipelinesOptions{ 380 Ref: gitlab.String(branch), 381 }) 382 if len(pipelines) == 0 || err != nil { 383 return nil, err 384 } 385 target := pipelines[0].ID 386 jobs, _, err := lab.Jobs.ListPipelineJobs(pid, target, &gitlab.ListJobsOptions{ 387 ListOptions: gitlab.ListOptions{ 388 PerPage: 500, 389 }, 390 }) 391 if err != nil { 392 return nil, err 393 } 394 return jobs, nil 395 } 396 397 // CITrace searches by name for a job and returns its trace file. The trace is 398 // static so may only be a portion of the logs if the job is till running. If 399 // no name is provided job is picked using the first available: 400 // 1. Last Running Job 401 // 2. First Pending Job 402 // 3. Last Job in Pipeline 403 func CITrace(pid interface{}, branch, name string) (io.Reader, *gitlab.Job, error) { 404 jobs, err := CIJobs(pid, branch) 405 if len(jobs) == 0 || err != nil { 406 return nil, nil, err 407 } 408 var ( 409 job *gitlab.Job 410 lastRunning *gitlab.Job 411 firstPending *gitlab.Job 412 ) 413 414 for _, j := range jobs { 415 if j.Status == "running" { 416 lastRunning = j 417 } 418 if j.Status == "pending" && firstPending == nil { 419 firstPending = j 420 } 421 if j.Name == name { 422 job = j 423 // don't break because there may be a newer version of the job 424 } 425 } 426 if job == nil { 427 job = lastRunning 428 } 429 if job == nil { 430 job = firstPending 431 } 432 if job == nil { 433 job = jobs[len(jobs)-1] 434 } 435 r, _, err := lab.Jobs.GetTraceFile(pid, job.ID) 436 if err != nil { 437 return nil, job, err 438 } 439 440 return r, job, err 441 } 442 443 // CIPlayOrRetry runs a job either by playing it for the first time or by 444 // retrying it based on the currently known job state 445 func CIPlayOrRetry(pid interface{}, jobID int, status string) (*gitlab.Job, error) { 446 switch status { 447 case "pending", "running": 448 return nil, nil 449 case "manual": 450 j, _, err := lab.Jobs.PlayJob(pid, jobID) 451 if err != nil { 452 return nil, err 453 } 454 return j, nil 455 default: 456 457 j, _, err := lab.Jobs.RetryJob(pid, jobID) 458 if err != nil { 459 return nil, err 460 } 461 462 return j, nil 463 } 464 } 465 466 // CICancel cancels a job for a given project by its ID. 467 func CICancel(pid interface{}, jobID int) (*gitlab.Job, error) { 468 j, _, err := lab.Jobs.CancelJob(pid, jobID) 469 if err != nil { 470 return nil, err 471 } 472 return j, nil 473 } 474 475 // UserIDFromUsername returns the associated Users ID in GitLab. This is useful 476 // for API calls that allow you to reference a user, but only by ID. 477 func UserIDFromUsername(username string) (int, error) { 478 us, _, err := lab.Users.ListUsers(&gitlab.ListUsersOptions{ 479 Username: gitlab.String(username), 480 }) 481 if err != nil || len(us) == 0 { 482 return -1, err 483 } 484 return us[0].ID, nil 485 }