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