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