gitlab.go

  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}