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