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)
 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}