1// 2// Copyright 2017, Sander van Harmelen 3// 4// Licensed under the Apache License, Version 2.0 (the "License"); 5// you may not use this file except in compliance with the License. 6// You may obtain a copy of the License at 7// 8// http://www.apache.org/licenses/LICENSE-2.0 9// 10// Unless required by applicable law or agreed to in writing, software 11// distributed under the License is distributed on an "AS IS" BASIS, 12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13// See the License for the specific language governing permissions and 14// limitations under the License. 15// 16 17package gitlab 18 19import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "net/http" 28 "net/url" 29 "sort" 30 "strconv" 31 "strings" 32 "time" 33 34 "github.com/google/go-querystring/query" 35 "golang.org/x/oauth2" 36) 37 38const ( 39 defaultBaseURL = "https://gitlab.com/" 40 apiVersionPath = "api/v4/" 41 userAgent = "go-gitlab" 42) 43 44// authType represents an authentication type within GitLab. 45// 46// GitLab API docs: https://docs.gitlab.com/ce/api/ 47type authType int 48 49// List of available authentication types. 50// 51// GitLab API docs: https://docs.gitlab.com/ce/api/ 52const ( 53 basicAuth authType = iota 54 oAuthToken 55 privateToken 56) 57 58// AccessLevelValue represents a permission level within GitLab. 59// 60// GitLab API docs: https://docs.gitlab.com/ce/permissions/permissions.html 61type AccessLevelValue int 62 63// List of available access levels 64// 65// GitLab API docs: https://docs.gitlab.com/ce/permissions/permissions.html 66const ( 67 NoPermissions AccessLevelValue = 0 68 GuestPermissions AccessLevelValue = 10 69 ReporterPermissions AccessLevelValue = 20 70 DeveloperPermissions AccessLevelValue = 30 71 MaintainerPermissions AccessLevelValue = 40 72 OwnerPermissions AccessLevelValue = 50 73 74 // These are deprecated and should be removed in a future version 75 MasterPermissions AccessLevelValue = 40 76 OwnerPermission AccessLevelValue = 50 77) 78 79// BuildStateValue represents a GitLab build state. 80type BuildStateValue string 81 82// These constants represent all valid build states. 83const ( 84 Pending BuildStateValue = "pending" 85 Running BuildStateValue = "running" 86 Success BuildStateValue = "success" 87 Failed BuildStateValue = "failed" 88 Canceled BuildStateValue = "canceled" 89 Skipped BuildStateValue = "skipped" 90) 91 92// ISOTime represents an ISO 8601 formatted date 93type ISOTime time.Time 94 95// ISO 8601 date format 96const iso8601 = "2006-01-02" 97 98// MarshalJSON implements the json.Marshaler interface 99func (t ISOTime) MarshalJSON() ([]byte, error) { 100 if y := time.Time(t).Year(); y < 0 || y >= 10000 { 101 // ISO 8901 uses 4 digits for the years 102 return nil, errors.New("ISOTime.MarshalJSON: year outside of range [0,9999]") 103 } 104 105 b := make([]byte, 0, len(iso8601)+2) 106 b = append(b, '"') 107 b = time.Time(t).AppendFormat(b, iso8601) 108 b = append(b, '"') 109 110 return b, nil 111} 112 113// UnmarshalJSON implements the json.Unmarshaler interface 114func (t *ISOTime) UnmarshalJSON(data []byte) error { 115 // Ignore null, like in the main JSON package 116 if string(data) == "null" { 117 return nil 118 } 119 120 isotime, err := time.Parse(`"`+iso8601+`"`, string(data)) 121 *t = ISOTime(isotime) 122 123 return err 124} 125 126// EncodeValues implements the query.Encoder interface 127func (t *ISOTime) EncodeValues(key string, v *url.Values) error { 128 if t == nil || (time.Time(*t)).IsZero() { 129 return nil 130 } 131 v.Add(key, t.String()) 132 return nil 133} 134 135// String implements the Stringer interface 136func (t ISOTime) String() string { 137 return time.Time(t).Format(iso8601) 138} 139 140// NotificationLevelValue represents a notification level. 141type NotificationLevelValue int 142 143// String implements the fmt.Stringer interface. 144func (l NotificationLevelValue) String() string { 145 return notificationLevelNames[l] 146} 147 148// MarshalJSON implements the json.Marshaler interface. 149func (l NotificationLevelValue) MarshalJSON() ([]byte, error) { 150 return json.Marshal(l.String()) 151} 152 153// UnmarshalJSON implements the json.Unmarshaler interface. 154func (l *NotificationLevelValue) UnmarshalJSON(data []byte) error { 155 var raw interface{} 156 if err := json.Unmarshal(data, &raw); err != nil { 157 return err 158 } 159 160 switch raw := raw.(type) { 161 case float64: 162 *l = NotificationLevelValue(raw) 163 case string: 164 *l = notificationLevelTypes[raw] 165 case nil: 166 // No action needed. 167 default: 168 return fmt.Errorf("json: cannot unmarshal %T into Go value of type %T", raw, *l) 169 } 170 171 return nil 172} 173 174// List of valid notification levels. 175const ( 176 DisabledNotificationLevel NotificationLevelValue = iota 177 ParticipatingNotificationLevel 178 WatchNotificationLevel 179 GlobalNotificationLevel 180 MentionNotificationLevel 181 CustomNotificationLevel 182) 183 184var notificationLevelNames = [...]string{ 185 "disabled", 186 "participating", 187 "watch", 188 "global", 189 "mention", 190 "custom", 191} 192 193var notificationLevelTypes = map[string]NotificationLevelValue{ 194 "disabled": DisabledNotificationLevel, 195 "participating": ParticipatingNotificationLevel, 196 "watch": WatchNotificationLevel, 197 "global": GlobalNotificationLevel, 198 "mention": MentionNotificationLevel, 199 "custom": CustomNotificationLevel, 200} 201 202// VisibilityValue represents a visibility level within GitLab. 203// 204// GitLab API docs: https://docs.gitlab.com/ce/api/ 205type VisibilityValue string 206 207// List of available visibility levels 208// 209// GitLab API docs: https://docs.gitlab.com/ce/api/ 210const ( 211 PrivateVisibility VisibilityValue = "private" 212 InternalVisibility VisibilityValue = "internal" 213 PublicVisibility VisibilityValue = "public" 214) 215 216// MergeMethodValue represents a project merge type within GitLab. 217// 218// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#project-merge-method 219type MergeMethodValue string 220 221// List of available merge type 222// 223// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#project-merge-method 224const ( 225 NoFastForwardMerge MergeMethodValue = "merge" 226 FastForwardMerge MergeMethodValue = "ff" 227 RebaseMerge MergeMethodValue = "rebase_merge" 228) 229 230// EventTypeValue represents actions type for contribution events 231type EventTypeValue string 232 233// List of available action type 234// 235// GitLab API docs: https://docs.gitlab.com/ce/api/events.html#action-types 236const ( 237 CreatedEventType EventTypeValue = "created" 238 UpdatedEventType EventTypeValue = "updated" 239 ClosedEventType EventTypeValue = "closed" 240 ReopenedEventType EventTypeValue = "reopened" 241 PushedEventType EventTypeValue = "pushed" 242 CommentedEventType EventTypeValue = "commented" 243 MergedEventType EventTypeValue = "merged" 244 JoinedEventType EventTypeValue = "joined" 245 LeftEventType EventTypeValue = "left" 246 DestroyedEventType EventTypeValue = "destroyed" 247 ExpiredEventType EventTypeValue = "expired" 248) 249 250// EventTargetTypeValue represents actions type value for contribution events 251type EventTargetTypeValue string 252 253// List of available action type 254// 255// GitLab API docs: https://docs.gitlab.com/ce/api/events.html#target-types 256const ( 257 IssueEventTargetType EventTargetTypeValue = "issue" 258 MilestoneEventTargetType EventTargetTypeValue = "milestone" 259 MergeRequestEventTargetType EventTargetTypeValue = "merge_request" 260 NoteEventTargetType EventTargetTypeValue = "note" 261 ProjectEventTargetType EventTargetTypeValue = "project" 262 SnippetEventTargetType EventTargetTypeValue = "snippet" 263 UserEventTargetType EventTargetTypeValue = "user" 264) 265 266// A Client manages communication with the GitLab API. 267type Client struct { 268 // HTTP client used to communicate with the API. 269 client *http.Client 270 271 // Base URL for API requests. Defaults to the public GitLab API, but can be 272 // set to a domain endpoint to use with a self hosted GitLab server. baseURL 273 // should always be specified with a trailing slash. 274 baseURL *url.URL 275 276 // Token type used to make authenticated API calls. 277 authType authType 278 279 // Username and password used for basix authentication. 280 username, password string 281 282 // Token used to make authenticated API calls. 283 token string 284 285 // User agent used when communicating with the GitLab API. 286 UserAgent string 287 288 // Services used for talking to different parts of the GitLab API. 289 AccessRequests *AccessRequestsService 290 AwardEmoji *AwardEmojiService 291 Boards *IssueBoardsService 292 Branches *BranchesService 293 BroadcastMessage *BroadcastMessagesService 294 BuildVariables *BuildVariablesService 295 CIYMLTemplate *CIYMLTemplatesService 296 Commits *CommitsService 297 ContainerRegistry *ContainerRegistryService 298 CustomAttribute *CustomAttributesService 299 DeployKeys *DeployKeysService 300 Deployments *DeploymentsService 301 Discussions *DiscussionsService 302 Environments *EnvironmentsService 303 Epics *EpicsService 304 Events *EventsService 305 Features *FeaturesService 306 GitIgnoreTemplates *GitIgnoreTemplatesService 307 GroupBadges *GroupBadgesService 308 GroupIssueBoards *GroupIssueBoardsService 309 GroupLabels *GroupLabelsService 310 GroupMembers *GroupMembersService 311 GroupMilestones *GroupMilestonesService 312 GroupVariables *GroupVariablesService 313 Groups *GroupsService 314 IssueLinks *IssueLinksService 315 Issues *IssuesService 316 Jobs *JobsService 317 Keys *KeysService 318 Labels *LabelsService 319 License *LicenseService 320 LicenseTemplates *LicenseTemplatesService 321 MergeRequestApprovals *MergeRequestApprovalsService 322 MergeRequests *MergeRequestsService 323 Milestones *MilestonesService 324 Namespaces *NamespacesService 325 Notes *NotesService 326 NotificationSettings *NotificationSettingsService 327 PagesDomains *PagesDomainsService 328 PipelineSchedules *PipelineSchedulesService 329 PipelineTriggers *PipelineTriggersService 330 Pipelines *PipelinesService 331 ProjectBadges *ProjectBadgesService 332 ProjectCluster *ProjectClustersService 333 ProjectImportExport *ProjectImportExportService 334 ProjectMembers *ProjectMembersService 335 ProjectSnippets *ProjectSnippetsService 336 ProjectVariables *ProjectVariablesService 337 Projects *ProjectsService 338 ProtectedBranches *ProtectedBranchesService 339 ProtectedTags *ProtectedTagsService 340 ReleaseLinks *ReleaseLinksService 341 Releases *ReleasesService 342 Repositories *RepositoriesService 343 RepositoryFiles *RepositoryFilesService 344 Runners *RunnersService 345 Search *SearchService 346 Services *ServicesService 347 Settings *SettingsService 348 Sidekiq *SidekiqService 349 Snippets *SnippetsService 350 SystemHooks *SystemHooksService 351 Tags *TagsService 352 Todos *TodosService 353 Users *UsersService 354 Validate *ValidateService 355 Version *VersionService 356 Wikis *WikisService 357} 358 359// ListOptions specifies the optional parameters to various List methods that 360// support pagination. 361type ListOptions struct { 362 // For paginated result sets, page of results to retrieve. 363 Page int `url:"page,omitempty" json:"page,omitempty"` 364 365 // For paginated result sets, the number of results to include per page. 366 PerPage int `url:"per_page,omitempty" json:"per_page,omitempty"` 367} 368 369// NewClient returns a new GitLab API client. If a nil httpClient is 370// provided, http.DefaultClient will be used. To use API methods which require 371// authentication, provide a valid private or personal token. 372func NewClient(httpClient *http.Client, token string) *Client { 373 client := newClient(httpClient) 374 client.authType = privateToken 375 client.token = token 376 return client 377} 378 379// NewBasicAuthClient returns a new GitLab API client. If a nil httpClient is 380// provided, http.DefaultClient will be used. To use API methods which require 381// authentication, provide a valid username and password. 382func NewBasicAuthClient(httpClient *http.Client, endpoint, username, password string) (*Client, error) { 383 client := newClient(httpClient) 384 client.authType = basicAuth 385 client.username = username 386 client.password = password 387 client.SetBaseURL(endpoint) 388 389 err := client.requestOAuthToken(context.TODO()) 390 if err != nil { 391 return nil, err 392 } 393 394 return client, nil 395} 396 397func (c *Client) requestOAuthToken(ctx context.Context) error { 398 config := &oauth2.Config{ 399 Endpoint: oauth2.Endpoint{ 400 AuthURL: fmt.Sprintf("%s://%s/oauth/authorize", c.BaseURL().Scheme, c.BaseURL().Host), 401 TokenURL: fmt.Sprintf("%s://%s/oauth/token", c.BaseURL().Scheme, c.BaseURL().Host), 402 }, 403 } 404 ctx = context.WithValue(ctx, oauth2.HTTPClient, c.client) 405 t, err := config.PasswordCredentialsToken(ctx, c.username, c.password) 406 if err != nil { 407 return err 408 } 409 c.token = t.AccessToken 410 return nil 411} 412 413// NewOAuthClient returns a new GitLab API client. If a nil httpClient is 414// provided, http.DefaultClient will be used. To use API methods which require 415// authentication, provide a valid oauth token. 416func NewOAuthClient(httpClient *http.Client, token string) *Client { 417 client := newClient(httpClient) 418 client.authType = oAuthToken 419 client.token = token 420 return client 421} 422 423func newClient(httpClient *http.Client) *Client { 424 if httpClient == nil { 425 httpClient = http.DefaultClient 426 } 427 428 c := &Client{client: httpClient, UserAgent: userAgent} 429 if err := c.SetBaseURL(defaultBaseURL); err != nil { 430 // Should never happen since defaultBaseURL is our constant. 431 panic(err) 432 } 433 434 // Create the internal timeStats service. 435 timeStats := &timeStatsService{client: c} 436 437 // Create all the public services. 438 c.AccessRequests = &AccessRequestsService{client: c} 439 c.AwardEmoji = &AwardEmojiService{client: c} 440 c.Boards = &IssueBoardsService{client: c} 441 c.Branches = &BranchesService{client: c} 442 c.BroadcastMessage = &BroadcastMessagesService{client: c} 443 c.BuildVariables = &BuildVariablesService{client: c} 444 c.CIYMLTemplate = &CIYMLTemplatesService{client: c} 445 c.Commits = &CommitsService{client: c} 446 c.ContainerRegistry = &ContainerRegistryService{client: c} 447 c.CustomAttribute = &CustomAttributesService{client: c} 448 c.DeployKeys = &DeployKeysService{client: c} 449 c.Deployments = &DeploymentsService{client: c} 450 c.Discussions = &DiscussionsService{client: c} 451 c.Environments = &EnvironmentsService{client: c} 452 c.Epics = &EpicsService{client: c} 453 c.Events = &EventsService{client: c} 454 c.Features = &FeaturesService{client: c} 455 c.GitIgnoreTemplates = &GitIgnoreTemplatesService{client: c} 456 c.GroupBadges = &GroupBadgesService{client: c} 457 c.GroupIssueBoards = &GroupIssueBoardsService{client: c} 458 c.GroupLabels = &GroupLabelsService{client: c} 459 c.GroupMembers = &GroupMembersService{client: c} 460 c.GroupMilestones = &GroupMilestonesService{client: c} 461 c.GroupVariables = &GroupVariablesService{client: c} 462 c.Groups = &GroupsService{client: c} 463 c.IssueLinks = &IssueLinksService{client: c} 464 c.Issues = &IssuesService{client: c, timeStats: timeStats} 465 c.Jobs = &JobsService{client: c} 466 c.Keys = &KeysService{client: c} 467 c.Labels = &LabelsService{client: c} 468 c.License = &LicenseService{client: c} 469 c.LicenseTemplates = &LicenseTemplatesService{client: c} 470 c.MergeRequestApprovals = &MergeRequestApprovalsService{client: c} 471 c.MergeRequests = &MergeRequestsService{client: c, timeStats: timeStats} 472 c.Milestones = &MilestonesService{client: c} 473 c.Namespaces = &NamespacesService{client: c} 474 c.Notes = &NotesService{client: c} 475 c.NotificationSettings = &NotificationSettingsService{client: c} 476 c.PagesDomains = &PagesDomainsService{client: c} 477 c.PipelineSchedules = &PipelineSchedulesService{client: c} 478 c.PipelineTriggers = &PipelineTriggersService{client: c} 479 c.Pipelines = &PipelinesService{client: c} 480 c.ProjectBadges = &ProjectBadgesService{client: c} 481 c.ProjectCluster = &ProjectClustersService{client: c} 482 c.ProjectImportExport = &ProjectImportExportService{client: c} 483 c.ProjectMembers = &ProjectMembersService{client: c} 484 c.ProjectSnippets = &ProjectSnippetsService{client: c} 485 c.ProjectVariables = &ProjectVariablesService{client: c} 486 c.Projects = &ProjectsService{client: c} 487 c.ProtectedBranches = &ProtectedBranchesService{client: c} 488 c.ProtectedTags = &ProtectedTagsService{client: c} 489 c.ReleaseLinks = &ReleaseLinksService{client: c} 490 c.Releases = &ReleasesService{client: c} 491 c.Repositories = &RepositoriesService{client: c} 492 c.RepositoryFiles = &RepositoryFilesService{client: c} 493 c.Runners = &RunnersService{client: c} 494 c.Search = &SearchService{client: c} 495 c.Services = &ServicesService{client: c} 496 c.Settings = &SettingsService{client: c} 497 c.Sidekiq = &SidekiqService{client: c} 498 c.Snippets = &SnippetsService{client: c} 499 c.SystemHooks = &SystemHooksService{client: c} 500 c.Tags = &TagsService{client: c} 501 c.Todos = &TodosService{client: c} 502 c.Users = &UsersService{client: c} 503 c.Validate = &ValidateService{client: c} 504 c.Version = &VersionService{client: c} 505 c.Wikis = &WikisService{client: c} 506 507 return c 508} 509 510// BaseURL return a copy of the baseURL. 511func (c *Client) BaseURL() *url.URL { 512 u := *c.baseURL 513 return &u 514} 515 516// SetBaseURL sets the base URL for API requests to a custom endpoint. urlStr 517// should always be specified with a trailing slash. 518func (c *Client) SetBaseURL(urlStr string) error { 519 // Make sure the given URL end with a slash 520 if !strings.HasSuffix(urlStr, "/") { 521 urlStr += "/" 522 } 523 524 baseURL, err := url.Parse(urlStr) 525 if err != nil { 526 return err 527 } 528 529 if !strings.HasSuffix(baseURL.Path, apiVersionPath) { 530 baseURL.Path += apiVersionPath 531 } 532 533 // Update the base URL of the client. 534 c.baseURL = baseURL 535 536 return nil 537} 538 539// NewRequest creates an API request. A relative URL path can be provided in 540// urlStr, in which case it is resolved relative to the base URL of the Client. 541// Relative URL paths should always be specified without a preceding slash. If 542// specified, the value pointed to by body is JSON encoded and included as the 543// request body. 544func (c *Client) NewRequest(method, path string, opt interface{}, options []OptionFunc) (*http.Request, error) { 545 u := *c.baseURL 546 unescaped, err := url.PathUnescape(path) 547 if err != nil { 548 return nil, err 549 } 550 551 // Set the encoded path data 552 u.RawPath = c.baseURL.Path + path 553 u.Path = c.baseURL.Path + unescaped 554 555 if opt != nil { 556 q, err := query.Values(opt) 557 if err != nil { 558 return nil, err 559 } 560 u.RawQuery = q.Encode() 561 } 562 563 req := &http.Request{ 564 Method: method, 565 URL: &u, 566 Proto: "HTTP/1.1", 567 ProtoMajor: 1, 568 ProtoMinor: 1, 569 Header: make(http.Header), 570 Host: u.Host, 571 } 572 573 for _, fn := range options { 574 if fn == nil { 575 continue 576 } 577 578 if err := fn(req); err != nil { 579 return nil, err 580 } 581 } 582 583 if method == "POST" || method == "PUT" { 584 bodyBytes, err := json.Marshal(opt) 585 if err != nil { 586 return nil, err 587 } 588 bodyReader := bytes.NewReader(bodyBytes) 589 590 u.RawQuery = "" 591 req.Body = ioutil.NopCloser(bodyReader) 592 req.GetBody = func() (io.ReadCloser, error) { 593 return ioutil.NopCloser(bodyReader), nil 594 } 595 req.ContentLength = int64(bodyReader.Len()) 596 req.Header.Set("Content-Type", "application/json") 597 } 598 599 req.Header.Set("Accept", "application/json") 600 601 switch c.authType { 602 case basicAuth, oAuthToken: 603 req.Header.Set("Authorization", "Bearer "+c.token) 604 case privateToken: 605 req.Header.Set("PRIVATE-TOKEN", c.token) 606 } 607 608 if c.UserAgent != "" { 609 req.Header.Set("User-Agent", c.UserAgent) 610 } 611 612 return req, nil 613} 614 615// Response is a GitLab API response. This wraps the standard http.Response 616// returned from GitLab and provides convenient access to things like 617// pagination links. 618type Response struct { 619 *http.Response 620 621 // These fields provide the page values for paginating through a set of 622 // results. Any or all of these may be set to the zero value for 623 // responses that are not part of a paginated set, or for which there 624 // are no additional pages. 625 TotalItems int 626 TotalPages int 627 ItemsPerPage int 628 CurrentPage int 629 NextPage int 630 PreviousPage int 631} 632 633// newResponse creates a new Response for the provided http.Response. 634func newResponse(r *http.Response) *Response { 635 response := &Response{Response: r} 636 response.populatePageValues() 637 return response 638} 639 640const ( 641 xTotal = "X-Total" 642 xTotalPages = "X-Total-Pages" 643 xPerPage = "X-Per-Page" 644 xPage = "X-Page" 645 xNextPage = "X-Next-Page" 646 xPrevPage = "X-Prev-Page" 647) 648 649// populatePageValues parses the HTTP Link response headers and populates the 650// various pagination link values in the Response. 651func (r *Response) populatePageValues() { 652 if totalItems := r.Response.Header.Get(xTotal); totalItems != "" { 653 r.TotalItems, _ = strconv.Atoi(totalItems) 654 } 655 if totalPages := r.Response.Header.Get(xTotalPages); totalPages != "" { 656 r.TotalPages, _ = strconv.Atoi(totalPages) 657 } 658 if itemsPerPage := r.Response.Header.Get(xPerPage); itemsPerPage != "" { 659 r.ItemsPerPage, _ = strconv.Atoi(itemsPerPage) 660 } 661 if currentPage := r.Response.Header.Get(xPage); currentPage != "" { 662 r.CurrentPage, _ = strconv.Atoi(currentPage) 663 } 664 if nextPage := r.Response.Header.Get(xNextPage); nextPage != "" { 665 r.NextPage, _ = strconv.Atoi(nextPage) 666 } 667 if previousPage := r.Response.Header.Get(xPrevPage); previousPage != "" { 668 r.PreviousPage, _ = strconv.Atoi(previousPage) 669 } 670} 671 672// Do sends an API request and returns the API response. The API response is 673// JSON decoded and stored in the value pointed to by v, or returned as an 674// error if an API error has occurred. If v implements the io.Writer 675// interface, the raw response body will be written to v, without attempting to 676// first decode it. 677func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { 678 resp, err := c.client.Do(req) 679 if err != nil { 680 return nil, err 681 } 682 defer resp.Body.Close() 683 684 if resp.StatusCode == http.StatusUnauthorized && c.authType == basicAuth { 685 err = c.requestOAuthToken(req.Context()) 686 if err != nil { 687 return nil, err 688 } 689 return c.Do(req, v) 690 } 691 692 response := newResponse(resp) 693 694 err = CheckResponse(resp) 695 if err != nil { 696 // even though there was an error, we still return the response 697 // in case the caller wants to inspect it further 698 return response, err 699 } 700 701 if v != nil { 702 if w, ok := v.(io.Writer); ok { 703 _, err = io.Copy(w, resp.Body) 704 } else { 705 err = json.NewDecoder(resp.Body).Decode(v) 706 } 707 } 708 709 return response, err 710} 711 712// Helper function to accept and format both the project ID or name as project 713// identifier for all API calls. 714func parseID(id interface{}) (string, error) { 715 switch v := id.(type) { 716 case int: 717 return strconv.Itoa(v), nil 718 case string: 719 return v, nil 720 default: 721 return "", fmt.Errorf("invalid ID type %#v, the ID must be an int or a string", id) 722 } 723} 724 725// Helper function to escape a project identifier. 726func pathEscape(s string) string { 727 return strings.Replace(url.PathEscape(s), ".", "%2E", -1) 728} 729 730// An ErrorResponse reports one or more errors caused by an API request. 731// 732// GitLab API docs: 733// https://docs.gitlab.com/ce/api/README.html#data-validation-and-error-reporting 734type ErrorResponse struct { 735 Body []byte 736 Response *http.Response 737 Message string 738} 739 740func (e *ErrorResponse) Error() string { 741 path, _ := url.QueryUnescape(e.Response.Request.URL.Path) 742 u := fmt.Sprintf("%s://%s%s", e.Response.Request.URL.Scheme, e.Response.Request.URL.Host, path) 743 return fmt.Sprintf("%s %s: %d %s", e.Response.Request.Method, u, e.Response.StatusCode, e.Message) 744} 745 746// CheckResponse checks the API response for errors, and returns them if present. 747func CheckResponse(r *http.Response) error { 748 switch r.StatusCode { 749 case 200, 201, 202, 204, 304: 750 return nil 751 } 752 753 errorResponse := &ErrorResponse{Response: r} 754 data, err := ioutil.ReadAll(r.Body) 755 if err == nil && data != nil { 756 errorResponse.Body = data 757 758 var raw interface{} 759 if err := json.Unmarshal(data, &raw); err != nil { 760 errorResponse.Message = "failed to parse unknown error format" 761 } else { 762 errorResponse.Message = parseError(raw) 763 } 764 } 765 766 return errorResponse 767} 768 769// Format: 770// { 771// "message": { 772// "<property-name>": [ 773// "<error-message>", 774// "<error-message>", 775// ... 776// ], 777// "<embed-entity>": { 778// "<property-name>": [ 779// "<error-message>", 780// "<error-message>", 781// ... 782// ], 783// } 784// }, 785// "error": "<error-message>" 786// } 787func parseError(raw interface{}) string { 788 switch raw := raw.(type) { 789 case string: 790 return raw 791 792 case []interface{}: 793 var errs []string 794 for _, v := range raw { 795 errs = append(errs, parseError(v)) 796 } 797 return fmt.Sprintf("[%s]", strings.Join(errs, ", ")) 798 799 case map[string]interface{}: 800 var errs []string 801 for k, v := range raw { 802 errs = append(errs, fmt.Sprintf("{%s: %s}", k, parseError(v))) 803 } 804 sort.Strings(errs) 805 return strings.Join(errs, ", ") 806 807 default: 808 return fmt.Sprintf("failed to parse unexpected error type: %T", raw) 809 } 810} 811 812// OptionFunc can be passed to all API requests to make the API call as if you were 813// another user, provided your private token is from an administrator account. 814// 815// GitLab docs: https://docs.gitlab.com/ce/api/README.html#sudo 816type OptionFunc func(*http.Request) error 817 818// WithSudo takes either a username or user ID and sets the SUDO request header 819func WithSudo(uid interface{}) OptionFunc { 820 return func(req *http.Request) error { 821 user, err := parseID(uid) 822 if err != nil { 823 return err 824 } 825 req.Header.Set("SUDO", user) 826 return nil 827 } 828} 829 830// WithContext runs the request with the provided context 831func WithContext(ctx context.Context) OptionFunc { 832 return func(req *http.Request) error { 833 *req = *req.WithContext(ctx) 834 return nil 835 } 836} 837 838// Bool is a helper routine that allocates a new bool value 839// to store v and returns a pointer to it. 840func Bool(v bool) *bool { 841 p := new(bool) 842 *p = v 843 return p 844} 845 846// Int is a helper routine that allocates a new int32 value 847// to store v and returns a pointer to it, but unlike Int32 848// its argument value is an int. 849func Int(v int) *int { 850 p := new(int) 851 *p = v 852 return p 853} 854 855// String is a helper routine that allocates a new string value 856// to store v and returns a pointer to it. 857func String(v string) *string { 858 p := new(string) 859 *p = v 860 return p 861} 862 863// AccessLevel is a helper routine that allocates a new AccessLevelValue 864// to store v and returns a pointer to it. 865func AccessLevel(v AccessLevelValue) *AccessLevelValue { 866 p := new(AccessLevelValue) 867 *p = v 868 return p 869} 870 871// BuildState is a helper routine that allocates a new BuildStateValue 872// to store v and returns a pointer to it. 873func BuildState(v BuildStateValue) *BuildStateValue { 874 p := new(BuildStateValue) 875 *p = v 876 return p 877} 878 879// NotificationLevel is a helper routine that allocates a new NotificationLevelValue 880// to store v and returns a pointer to it. 881func NotificationLevel(v NotificationLevelValue) *NotificationLevelValue { 882 p := new(NotificationLevelValue) 883 *p = v 884 return p 885} 886 887// Visibility is a helper routine that allocates a new VisibilityValue 888// to store v and returns a pointer to it. 889func Visibility(v VisibilityValue) *VisibilityValue { 890 p := new(VisibilityValue) 891 *p = v 892 return p 893} 894 895// MergeMethod is a helper routine that allocates a new MergeMethod 896// to sotre v and returns a pointer to it. 897func MergeMethod(v MergeMethodValue) *MergeMethodValue { 898 p := new(MergeMethodValue) 899 *p = v 900 return p 901} 902 903// BoolValue is a boolean value with advanced json unmarshaling features. 904type BoolValue bool 905 906// UnmarshalJSON allows 1 and 0 to be considered as boolean values 907// Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/50122 908func (t *BoolValue) UnmarshalJSON(b []byte) error { 909 switch string(b) { 910 case `"1"`: 911 *t = true 912 return nil 913 case `"0"`: 914 *t = false 915 return nil 916 default: 917 var v bool 918 err := json.Unmarshal(b, &v) 919 *t = BoolValue(v) 920 return err 921 } 922} 923