gitlab.com/prarit/lab@v0.14.0/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 gitlab "github.com/xanzy/go-gitlab" 19 "github.com/zaquestion/lab/internal/git" 20 ) 21 22 var ( 23 // ErrProjectNotFound is returned when a GitLab project cannot be found. 24 ErrProjectNotFound = errors.New("gitlab project not found") 25 ) 26 27 var ( 28 lab *gitlab.Client 29 host string 30 user string 31 token string 32 ) 33 34 // Host exposes the GitLab scheme://hostname used to interact with the API 35 func Host() string { 36 return host 37 } 38 39 // User exposes the configured GitLab user 40 func User() string { 41 return user 42 } 43 44 // Init initializes a gitlab client for use throughout lab. 45 func Init(_host, _user, _token string) { 46 if len(_host) > 0 && _host[len(_host)-1 : len(_host)][0] == '/' { 47 _host = _host[0 : len(_host)-1] 48 } 49 host = _host 50 user = _user 51 token = _token 52 lab = gitlab.NewClient(nil, _token) 53 lab.SetBaseURL(host + "/api/v4") 54 } 55 56 // Defines filepath for default GitLab templates 57 const ( 58 TmplMR = "merge_request_templates/default.md" 59 TmplIssue = "issue_templates/default.md" 60 ) 61 62 // LoadGitLabTmpl loads gitlab templates for use in creating Issues and MRs 63 // 64 // https://gitlab.com/help/user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests 65 func LoadGitLabTmpl(tmplName string) string { 66 wd, err := git.WorkingDir() 67 if err != nil { 68 log.Fatal(err) 69 } 70 71 tmplFile := filepath.Join(wd, ".gitlab", tmplName) 72 f, err := os.Open(tmplFile) 73 if os.IsNotExist(err) { 74 return "" 75 } else if err != nil { 76 log.Fatal(err) 77 } 78 79 tmpl, err := ioutil.ReadAll(f) 80 if err != nil { 81 log.Fatal(err) 82 } 83 84 return strings.TrimSpace(string(tmpl)) 85 } 86 87 var ( 88 localProjects map[string]*gitlab.Project = make(map[string]*gitlab.Project) 89 ) 90 91 // GetProject looks up a Gitlab project by ID. 92 func GetProject(projectID interface{}) (*gitlab.Project, error) { 93 target, resp, err := lab.Projects.GetProject(projectID) 94 if resp != nil && resp.StatusCode == http.StatusNotFound { 95 return nil, ErrProjectNotFound 96 } 97 if err != nil { 98 return nil, err 99 } 100 return target, nil 101 } 102 103 // FindProject looks up the Gitlab project. If the namespace is not provided in 104 // the project string it will search for projects in the users namespace 105 func FindProject(project string) (*gitlab.Project, error) { 106 if target, ok := localProjects[project]; ok { 107 return target, nil 108 } 109 110 search := project 111 // Assuming that a "/" in the project means its owned by an org 112 if !strings.Contains(project, "/") { 113 search = user + "/" + project 114 } 115 116 target, resp, err := lab.Projects.GetProject(search) 117 if resp != nil && resp.StatusCode == http.StatusNotFound { 118 return nil, ErrProjectNotFound 119 } 120 if err != nil { 121 return nil, err 122 } 123 // fwiw, I feel bad about this 124 localProjects[project] = target 125 126 return target, nil 127 } 128 129 // Fork creates a user fork of a GitLab project 130 func Fork(project string) (string, error) { 131 if !strings.Contains(project, "/") { 132 return "", errors.New("remote must include namespace") 133 } 134 parts := strings.Split(project, "/") 135 136 // See if a fork already exists 137 target, err := FindProject(parts[1]) 138 if err == nil { 139 return target.SSHURLToRepo, nil 140 } else if err != nil && err != ErrProjectNotFound { 141 return "", err 142 } 143 144 target, err = FindProject(project) 145 if err != nil { 146 return "", err 147 } 148 149 fork, _, err := lab.Projects.ForkProject(target.ID) 150 if err != nil { 151 return "", err 152 } 153 154 return fork.SSHURLToRepo, nil 155 } 156 157 // MRCreate opens a merge request on GitLab 158 func MRCreate(project string, opts *gitlab.CreateMergeRequestOptions) (string, error) { 159 p, err := FindProject(project) 160 if err != nil { 161 return "", err 162 } 163 164 mr, _, err := lab.MergeRequests.CreateMergeRequest(p.ID, opts) 165 if err != nil { 166 return "", err 167 } 168 return mr.WebURL, nil 169 } 170 171 // MRGet retrieves the merge request from GitLab project 172 func MRGet(project string, mrNum int) (*gitlab.MergeRequest, error) { 173 p, err := FindProject(project) 174 if err != nil { 175 return nil, err 176 } 177 178 mr, _, err := lab.MergeRequests.GetMergeRequest(p.ID, mrNum) 179 if err != nil { 180 return nil, err 181 } 182 183 return mr, nil 184 } 185 186 // MRList lists the MRs on a GitLab project 187 func MRList(project string, opts gitlab.ListProjectMergeRequestsOptions, n int) ([]*gitlab.MergeRequest, error) { 188 if n == -1 { 189 opts.PerPage = 100 190 } 191 p, err := FindProject(project) 192 if err != nil { 193 return nil, err 194 } 195 196 list, resp, err := lab.MergeRequests.ListProjectMergeRequests(p.ID, &opts) 197 if err != nil { 198 return nil, err 199 } 200 if resp.CurrentPage == resp.TotalPages { 201 return list, nil 202 } 203 opts.Page = resp.NextPage 204 for len(list) < n || n == -1 { 205 if n != -1 { 206 opts.PerPage = n - len(list) 207 } 208 mrs, resp, err := lab.MergeRequests.ListProjectMergeRequests(p.ID, &opts) 209 if err != nil { 210 return nil, err 211 } 212 opts.Page = resp.NextPage 213 list = append(list, mrs...) 214 if resp.CurrentPage == resp.TotalPages { 215 break 216 } 217 } 218 return list, nil 219 } 220 221 // MRClose closes an mr on a GitLab project 222 func MRClose(pid interface{}, id int) error { 223 mr, _, err := lab.MergeRequests.GetMergeRequest(pid, id) 224 if err != nil { 225 return err 226 } 227 if mr.State == "closed" { 228 return fmt.Errorf("mr already closed") 229 } 230 _, _, err = lab.MergeRequests.UpdateMergeRequest(pid, int(id), &gitlab.UpdateMergeRequestOptions{ 231 StateEvent: gitlab.String("close"), 232 }) 233 if err != nil { 234 return err 235 } 236 return nil 237 } 238 239 // MRMerge merges an mr on a GitLab project 240 func MRMerge(pid interface{}, id int) error { 241 _, _, err := lab.MergeRequests.AcceptMergeRequest(pid, int(id), &gitlab.AcceptMergeRequestOptions{ 242 MergeWhenPipelineSucceeds: gitlab.Bool(true), 243 }) 244 if err != nil { 245 return err 246 } 247 return nil 248 } 249 250 // MRApprove approves an mr on a GitLab project 251 func MRApprove(pid interface{}, id int) error { 252 _, _, err := lab.MergeRequestApprovals.ApproveMergeRequest(pid, id, &gitlab.ApproveMergeRequestOptions{}) 253 if err != nil { 254 return err 255 } 256 return nil 257 } 258 259 // MRThumbUp places a thumb up/down on a merge request 260 func MRThumbUp(pid interface{}, id int) error { 261 _, _, err := lab.AwardEmoji.CreateMergeRequestAwardEmoji(pid, id, &gitlab.CreateAwardEmojiOptions{ 262 Name: "thumbsup", 263 }) 264 if err != nil { 265 return err 266 } 267 return nil 268 } 269 270 // MRThumbDown places a thumb up/down on a merge request 271 func MRThumbDown(pid interface{}, id int) error { 272 _, _, err := lab.AwardEmoji.CreateMergeRequestAwardEmoji(pid, id, &gitlab.CreateAwardEmojiOptions{ 273 Name: "thumbsdown", 274 }) 275 if err != nil { 276 return err 277 } 278 return nil 279 } 280 281 // IssueCreate opens a new issue on a GitLab project 282 func IssueCreate(project string, opts *gitlab.CreateIssueOptions) (string, error) { 283 p, err := FindProject(project) 284 if err != nil { 285 return "", err 286 } 287 288 mr, _, err := lab.Issues.CreateIssue(p.ID, opts) 289 if err != nil { 290 return "", err 291 } 292 return mr.WebURL, nil 293 } 294 295 // IssueGet retrieves the issue information from a GitLab project 296 func IssueGet(project string, issueNum int) (*gitlab.Issue, error) { 297 p, err := FindProject(project) 298 if err != nil { 299 return nil, err 300 } 301 302 issue, _, err := lab.Issues.GetIssue(p.ID, issueNum) 303 if err != nil { 304 return nil, err 305 } 306 307 return issue, nil 308 } 309 310 // IssueList gets a list of issues on a GitLab Project 311 func IssueList(project string, opts gitlab.ListProjectIssuesOptions, n int) ([]*gitlab.Issue, error) { 312 if n == -1 { 313 opts.PerPage = 100 314 } 315 p, err := FindProject(project) 316 if err != nil { 317 return nil, err 318 } 319 320 list, resp, err := lab.Issues.ListProjectIssues(p.ID, &opts) 321 if err != nil { 322 return nil, err 323 } 324 if resp.CurrentPage == resp.TotalPages { 325 return list, nil 326 } 327 328 opts.Page = resp.NextPage 329 for len(list) < n || n == -1 { 330 if n != -1 { 331 opts.PerPage = n - len(list) 332 } 333 issues, resp, err := lab.Issues.ListProjectIssues(p.ID, &opts) 334 if err != nil { 335 return nil, err 336 } 337 opts.Page = resp.NextPage 338 list = append(list, issues...) 339 if resp.CurrentPage == resp.TotalPages { 340 break 341 } 342 } 343 return list, nil 344 } 345 346 // IssueClose closes an issue on a GitLab project 347 func IssueClose(pid interface{}, id int) error { 348 _, err := lab.Issues.DeleteIssue(pid, int(id)) 349 if err != nil { 350 return err 351 } 352 return nil 353 } 354 355 // BranchPushed checks if a branch exists on a GitLab project 356 func BranchPushed(pid interface{}, branch string) bool { 357 b, _, err := lab.Branches.GetBranch(pid, branch) 358 if err != nil { 359 return false 360 } 361 return b != nil 362 } 363 364 // ProjectSnippetCreate creates a snippet in a project 365 func ProjectSnippetCreate(pid interface{}, opts *gitlab.CreateProjectSnippetOptions) (*gitlab.Snippet, error) { 366 snip, _, err := lab.ProjectSnippets.CreateSnippet(pid, opts) 367 if err != nil { 368 return nil, err 369 } 370 371 return snip, nil 372 } 373 374 // ProjectSnippetDelete deletes a project snippet 375 func ProjectSnippetDelete(pid interface{}, id int) error { 376 _, err := lab.ProjectSnippets.DeleteSnippet(pid, id) 377 return err 378 } 379 380 // ProjectSnippetList lists snippets on a project 381 func ProjectSnippetList(pid interface{}, opts gitlab.ListProjectSnippetsOptions, n int) ([]*gitlab.Snippet, error) { 382 if n == -1 { 383 opts.PerPage = 100 384 } 385 list, resp, err := lab.ProjectSnippets.ListSnippets(pid, &opts) 386 if err != nil { 387 return nil, err 388 } 389 if resp.CurrentPage == resp.TotalPages { 390 return list, nil 391 } 392 opts.Page = resp.NextPage 393 for len(list) < n || n == -1 { 394 if n != -1 { 395 opts.PerPage = n - len(list) 396 } 397 snips, resp, err := lab.ProjectSnippets.ListSnippets(pid, &opts) 398 if err != nil { 399 return nil, err 400 } 401 opts.Page = resp.NextPage 402 list = append(list, snips...) 403 if resp.CurrentPage == resp.TotalPages { 404 break 405 } 406 } 407 return list, nil 408 } 409 410 // SnippetCreate creates a personal snippet 411 func SnippetCreate(opts *gitlab.CreateSnippetOptions) (*gitlab.Snippet, error) { 412 snip, _, err := lab.Snippets.CreateSnippet(opts) 413 if err != nil { 414 return nil, err 415 } 416 417 return snip, nil 418 } 419 420 // SnippetDelete deletes a personal snippet 421 func SnippetDelete(id int) error { 422 _, err := lab.Snippets.DeleteSnippet(id) 423 return err 424 } 425 426 // SnippetList lists snippets on a project 427 func SnippetList(opts gitlab.ListSnippetsOptions, n int) ([]*gitlab.Snippet, error) { 428 if n == -1 { 429 opts.PerPage = 100 430 } 431 list, resp, err := lab.Snippets.ListSnippets(&opts) 432 if err != nil { 433 return nil, err 434 } 435 if resp.CurrentPage == resp.TotalPages { 436 return list, nil 437 } 438 opts.Page = resp.NextPage 439 for len(list) < n || n == -1 { 440 if n != -1 { 441 opts.PerPage = n - len(list) 442 } 443 snips, resp, err := lab.Snippets.ListSnippets(&opts) 444 if err != nil { 445 return nil, err 446 } 447 opts.Page = resp.NextPage 448 list = append(list, snips...) 449 if resp.CurrentPage == resp.TotalPages { 450 break 451 } 452 } 453 return list, nil 454 } 455 456 // Lint validates .gitlab-ci.yml contents 457 func Lint(content string) (bool, error) { 458 lint, _, err := lab.Validate.Lint(content) 459 if err != nil { 460 return false, err 461 } 462 if len(lint.Errors) > 0 { 463 return false, errors.New(strings.Join(lint.Errors, " - ")) 464 } 465 return lint.Status == "valid", nil 466 } 467 468 // ProjectCreate creates a new project on GitLab 469 func ProjectCreate(opts *gitlab.CreateProjectOptions) (*gitlab.Project, error) { 470 p, _, err := lab.Projects.CreateProject(opts) 471 if err != nil { 472 return nil, err 473 } 474 return p, nil 475 } 476 477 // ProjectDelete creates a new project on GitLab 478 func ProjectDelete(pid interface{}) error { 479 _, err := lab.Projects.DeleteProject(pid) 480 if err != nil { 481 return err 482 } 483 return nil 484 } 485 486 // ProjectList gets a list of projects on GitLab 487 func ProjectList(opts gitlab.ListProjectsOptions, n int) ([]*gitlab.Project, error) { 488 list, resp, err := lab.Projects.ListProjects(&opts) 489 if err != nil { 490 return nil, err 491 } 492 if resp.CurrentPage == resp.TotalPages { 493 return list, nil 494 } 495 opts.Page = resp.NextPage 496 for len(list) < n || n == -1 { 497 if n != -1 { 498 opts.PerPage = n - len(list) 499 } 500 projects, resp, err := lab.Projects.ListProjects(&opts) 501 if err != nil { 502 return nil, err 503 } 504 opts.Page = resp.NextPage 505 list = append(list, projects...) 506 if resp.CurrentPage == resp.TotalPages { 507 break 508 } 509 } 510 return list, nil 511 } 512 513 // CIJobs returns a list of jobs in a pipeline for a given sha. The jobs are 514 // returned sorted by their CreatedAt time 515 func CIJobs(pid interface{}, branch string) ([]*gitlab.Job, error) { 516 pipelines, _, err := lab.Pipelines.ListProjectPipelines(pid, &gitlab.ListProjectPipelinesOptions{ 517 Ref: gitlab.String(branch), 518 }) 519 if len(pipelines) == 0 || err != nil { 520 return nil, err 521 } 522 target := pipelines[0].ID 523 opts := &gitlab.ListJobsOptions{ 524 ListOptions: gitlab.ListOptions{ 525 PerPage: 500, 526 }, 527 } 528 list, resp, err := lab.Jobs.ListPipelineJobs(pid, target, opts) 529 if err != nil { 530 return nil, err 531 } 532 if resp.CurrentPage == resp.TotalPages { 533 return list, nil 534 } 535 opts.Page = resp.NextPage 536 for { 537 jobs, resp, err := lab.Jobs.ListPipelineJobs(pid, target, opts) 538 if err != nil { 539 return nil, err 540 } 541 opts.Page = resp.NextPage 542 list = append(list, jobs...) 543 if resp.CurrentPage == resp.TotalPages { 544 break 545 } 546 } 547 return list, nil 548 } 549 550 // CITrace searches by name for a job and returns its trace file. The trace is 551 // static so may only be a portion of the logs if the job is till running. If 552 // no name is provided job is picked using the first available: 553 // 1. Last Running Job 554 // 2. First Pending Job 555 // 3. Last Job in Pipeline 556 func CITrace(pid interface{}, branch, name string) (io.Reader, *gitlab.Job, error) { 557 jobs, err := CIJobs(pid, branch) 558 if len(jobs) == 0 || err != nil { 559 return nil, nil, err 560 } 561 var ( 562 job *gitlab.Job 563 lastRunning *gitlab.Job 564 firstPending *gitlab.Job 565 ) 566 567 for _, j := range jobs { 568 if j.Status == "running" { 569 lastRunning = j 570 } 571 if j.Status == "pending" && firstPending == nil { 572 firstPending = j 573 } 574 if j.Name == name { 575 job = j 576 // don't break because there may be a newer version of the job 577 } 578 } 579 if job == nil { 580 job = lastRunning 581 } 582 if job == nil { 583 job = firstPending 584 } 585 if job == nil { 586 job = jobs[len(jobs)-1] 587 } 588 r, _, err := lab.Jobs.GetTraceFile(pid, job.ID) 589 if err != nil { 590 return nil, job, err 591 } 592 593 return r, job, err 594 } 595 596 // CIPlayOrRetry runs a job either by playing it for the first time or by 597 // retrying it based on the currently known job state 598 func CIPlayOrRetry(pid interface{}, jobID int, status string) (*gitlab.Job, error) { 599 switch status { 600 case "pending", "running": 601 return nil, nil 602 case "manual": 603 j, _, err := lab.Jobs.PlayJob(pid, jobID) 604 if err != nil { 605 return nil, err 606 } 607 return j, nil 608 default: 609 610 j, _, err := lab.Jobs.RetryJob(pid, jobID) 611 if err != nil { 612 return nil, err 613 } 614 615 return j, nil 616 } 617 } 618 619 // CICancel cancels a job for a given project by its ID. 620 func CICancel(pid interface{}, jobID int) (*gitlab.Job, error) { 621 j, _, err := lab.Jobs.CancelJob(pid, jobID) 622 if err != nil { 623 return nil, err 624 } 625 return j, nil 626 } 627 628 // CICreate creates a pipeline for given ref 629 func CICreate(pid interface{}, opts *gitlab.CreatePipelineOptions) (*gitlab.Pipeline, error) { 630 p, _, err := lab.Pipelines.CreatePipeline(pid, opts) 631 if err != nil { 632 return nil, err 633 } 634 return p, nil 635 } 636 637 // CITrigger triggers a pipeline for given ref 638 func CITrigger(pid interface{}, opts gitlab.RunPipelineTriggerOptions) (*gitlab.Pipeline, error) { 639 p, _, err := lab.PipelineTriggers.RunPipelineTrigger(pid, &opts) 640 if err != nil { 641 return nil, err 642 } 643 return p, nil 644 } 645 646 // UserIDFromUsername returns the associated Users ID in GitLab. This is useful 647 // for API calls that allow you to reference a user, but only by ID. 648 func UserIDFromUsername(username string) (int, error) { 649 us, _, err := lab.Users.ListUsers(&gitlab.ListUsersOptions{ 650 Username: gitlab.String(username), 651 }) 652 if err != nil || len(us) == 0 { 653 return -1, err 654 } 655 return us[0].ID, nil 656 }