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