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