1// Copyright 2013 The go-github AUTHORS. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file.
5
6//go:generate go run gen-accessors.go
7//go:generate go run gen-stringify-test.go
8
9package github
10
11import (
12	"bytes"
13	"context"
14	"encoding/json"
15	"errors"
16	"fmt"
17	"io"
18	"io/ioutil"
19	"net/http"
20	"net/url"
21	"reflect"
22	"strconv"
23	"strings"
24	"sync"
25	"time"
26
27	"github.com/google/go-querystring/query"
28)
29
30const (
31	defaultBaseURL = "https://api.github.com/"
32	uploadBaseURL  = "https://uploads.github.com/"
33	userAgent      = "go-github"
34
35	headerRateLimit     = "X-RateLimit-Limit"
36	headerRateRemaining = "X-RateLimit-Remaining"
37	headerRateReset     = "X-RateLimit-Reset"
38	headerOTP           = "X-GitHub-OTP"
39
40	mediaTypeV3                = "application/vnd.github.v3+json"
41	defaultMediaType           = "application/octet-stream"
42	mediaTypeV3SHA             = "application/vnd.github.v3.sha"
43	mediaTypeV3Diff            = "application/vnd.github.v3.diff"
44	mediaTypeV3Patch           = "application/vnd.github.v3.patch"
45	mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json"
46
47	// Media Type values to access preview APIs
48
49	// https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/
50	mediaTypeStarringPreview = "application/vnd.github.v3.star+json"
51
52	// https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/
53	mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json"
54
55	// https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/
56	mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json"
57
58	// https://developer.github.com/changes/2018-10-16-deployments-environments-states-and-auto-inactive-updates/
59	mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json"
60
61	// https://developer.github.com/changes/2016-02-19-source-import-preview-api/
62	mediaTypeImportPreview = "application/vnd.github.barred-rock-preview"
63
64	// https://developer.github.com/changes/2016-05-12-reactions-api-preview/
65	mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview"
66
67	// https://developer.github.com/changes/2016-05-23-timeline-preview-api/
68	mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json"
69
70	// https://developer.github.com/changes/2016-07-06-github-pages-preiew-api/
71	mediaTypePagesPreview = "application/vnd.github.mister-fantastic-preview+json"
72
73	// https://developer.github.com/changes/2016-09-14-projects-api/
74	mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json"
75
76	// https://developer.github.com/changes/2016-09-14-Integrations-Early-Access/
77	mediaTypeIntegrationPreview = "application/vnd.github.machine-man-preview+json"
78
79	// https://developer.github.com/changes/2017-01-05-commit-search-api/
80	mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json"
81
82	// https://developer.github.com/changes/2017-02-28-user-blocking-apis-and-webhook/
83	mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json"
84
85	// https://developer.github.com/changes/2017-02-09-community-health/
86	mediaTypeRepositoryCommunityHealthMetricsPreview = "application/vnd.github.black-panther-preview+json"
87
88	// https://developer.github.com/changes/2017-05-23-coc-api/
89	mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json"
90
91	// https://developer.github.com/changes/2017-07-17-update-topics-on-repositories/
92	mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json"
93
94	// https://developer.github.com/changes/2017-08-30-preview-nested-teams/
95	mediaTypeNestedTeamsPreview = "application/vnd.github.hellcat-preview+json"
96
97	// https://developer.github.com/changes/2017-11-09-repository-transfer-api-preview/
98	mediaTypeRepositoryTransferPreview = "application/vnd.github.nightshade-preview+json"
99
100	// https://developer.github.com/changes/2018-01-25-organization-invitation-api-preview/
101	mediaTypeOrganizationInvitationPreview = "application/vnd.github.dazzler-preview+json"
102
103	// https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/
104	mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json"
105
106	// https://developer.github.com/changes/2018-02-22-label-description-search-preview/
107	mediaTypeLabelDescriptionSearchPreview = "application/vnd.github.symmetra-preview+json"
108
109	// https://developer.github.com/changes/2018-02-07-team-discussions-api/
110	mediaTypeTeamDiscussionsPreview = "application/vnd.github.echo-preview+json"
111
112	// https://developer.github.com/changes/2018-03-21-hovercard-api-preview/
113	mediaTypeHovercardPreview = "application/vnd.github.hagar-preview+json"
114
115	// https://developer.github.com/changes/2018-01-10-lock-reason-api-preview/
116	mediaTypeLockReasonPreview = "application/vnd.github.sailor-v-preview+json"
117
118	// https://developer.github.com/changes/2018-05-07-new-checks-api-public-beta/
119	mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json"
120
121	// https://developer.github.com/enterprise/2.13/v3/repos/pre_receive_hooks/
122	mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview"
123
124	// https://developer.github.com/changes/2018-02-22-protected-branches-required-signatures/
125	mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json"
126
127	// https://developer.github.com/changes/2018-09-05-project-card-events/
128	mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json"
129
130	// https://developer.github.com/changes/2018-12-18-interactions-preview/
131	mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json"
132
133	// https://developer.github.com/changes/2019-02-14-draft-pull-requests/
134	mediaTypeDraftPreview = "application/vnd.github.shadow-cat-preview+json"
135
136	// https://developer.github.com/changes/2019-03-14-enabling-disabling-pages/
137	mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json"
138
139	// https://developer.github.com/changes/2019-04-24-vulnerability-alerts/
140	mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json"
141
142	// https://developer.github.com/changes/2019-06-04-automated-security-fixes/
143	mediaTypeRequiredAutomatedSecurityFixesPreview = "application/vnd.github.london-preview+json"
144
145	// https://developer.github.com/changes/2019-05-29-update-branch-api/
146	mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json"
147
148	// https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/
149	mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json"
150
151	// https://developer.github.com/changes/2019-06-12-team-sync/
152	mediaTypeTeamSyncPreview = "application/vnd.github.team-sync-preview+json"
153
154	// https://developer.github.com/v3/previews/#repository-creation-permissions
155	mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json"
156
157	// https://developer.github.com/v3/previews/#create-and-use-repository-templates
158	mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json"
159)
160
161// A Client manages communication with the GitHub API.
162type Client struct {
163	clientMu sync.Mutex   // clientMu protects the client during calls that modify the CheckRedirect func.
164	client   *http.Client // HTTP client used to communicate with the API.
165
166	// Base URL for API requests. Defaults to the public GitHub API, but can be
167	// set to a domain endpoint to use with GitHub Enterprise. BaseURL should
168	// always be specified with a trailing slash.
169	BaseURL *url.URL
170
171	// Base URL for uploading files.
172	UploadURL *url.URL
173
174	// User agent used when communicating with the GitHub API.
175	UserAgent string
176
177	rateMu     sync.Mutex
178	rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls.
179
180	common service // Reuse a single struct instead of allocating one for each service on the heap.
181
182	// Services used for talking to different parts of the GitHub API.
183	Activity       *ActivityService
184	Admin          *AdminService
185	Apps           *AppsService
186	Authorizations *AuthorizationsService
187	Checks         *ChecksService
188	Gists          *GistsService
189	Git            *GitService
190	Gitignores     *GitignoresService
191	Interactions   *InteractionsService
192	Issues         *IssuesService
193	Licenses       *LicensesService
194	Marketplace    *MarketplaceService
195	Migrations     *MigrationService
196	Organizations  *OrganizationsService
197	Projects       *ProjectsService
198	PullRequests   *PullRequestsService
199	Reactions      *ReactionsService
200	Repositories   *RepositoriesService
201	Search         *SearchService
202	Teams          *TeamsService
203	Users          *UsersService
204}
205
206type service struct {
207	client *Client
208}
209
210// ListOptions specifies the optional parameters to various List methods that
211// support pagination.
212type ListOptions struct {
213	// For paginated result sets, page of results to retrieve.
214	Page int `url:"page,omitempty"`
215
216	// For paginated result sets, the number of results to include per page.
217	PerPage int `url:"per_page,omitempty"`
218}
219
220// UploadOptions specifies the parameters to methods that support uploads.
221type UploadOptions struct {
222	Name      string `url:"name,omitempty"`
223	Label     string `url:"label,omitempty"`
224	MediaType string `url:"-"`
225}
226
227// RawType represents type of raw format of a request instead of JSON.
228type RawType uint8
229
230const (
231	// Diff format.
232	Diff RawType = 1 + iota
233	// Patch format.
234	Patch
235)
236
237// RawOptions specifies parameters when user wants to get raw format of
238// a response instead of JSON.
239type RawOptions struct {
240	Type RawType
241}
242
243// addOptions adds the parameters in opt as URL query parameters to s. opt
244// must be a struct whose fields may contain "url" tags.
245func addOptions(s string, opt interface{}) (string, error) {
246	v := reflect.ValueOf(opt)
247	if v.Kind() == reflect.Ptr && v.IsNil() {
248		return s, nil
249	}
250
251	u, err := url.Parse(s)
252	if err != nil {
253		return s, err
254	}
255
256	qs, err := query.Values(opt)
257	if err != nil {
258		return s, err
259	}
260
261	u.RawQuery = qs.Encode()
262	return u.String(), nil
263}
264
265// NewClient returns a new GitHub API client. If a nil httpClient is
266// provided, a new http.Client will be used. To use API methods which require
267// authentication, provide an http.Client that will perform the authentication
268// for you (such as that provided by the golang.org/x/oauth2 library).
269func NewClient(httpClient *http.Client) *Client {
270	if httpClient == nil {
271		httpClient = &http.Client{}
272	}
273	baseURL, _ := url.Parse(defaultBaseURL)
274	uploadURL, _ := url.Parse(uploadBaseURL)
275
276	c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent, UploadURL: uploadURL}
277	c.common.client = c
278	c.Activity = (*ActivityService)(&c.common)
279	c.Admin = (*AdminService)(&c.common)
280	c.Apps = (*AppsService)(&c.common)
281	c.Authorizations = (*AuthorizationsService)(&c.common)
282	c.Checks = (*ChecksService)(&c.common)
283	c.Gists = (*GistsService)(&c.common)
284	c.Git = (*GitService)(&c.common)
285	c.Gitignores = (*GitignoresService)(&c.common)
286	c.Interactions = (*InteractionsService)(&c.common)
287	c.Issues = (*IssuesService)(&c.common)
288	c.Licenses = (*LicensesService)(&c.common)
289	c.Marketplace = &MarketplaceService{client: c}
290	c.Migrations = (*MigrationService)(&c.common)
291	c.Organizations = (*OrganizationsService)(&c.common)
292	c.Projects = (*ProjectsService)(&c.common)
293	c.PullRequests = (*PullRequestsService)(&c.common)
294	c.Reactions = (*ReactionsService)(&c.common)
295	c.Repositories = (*RepositoriesService)(&c.common)
296	c.Search = (*SearchService)(&c.common)
297	c.Teams = (*TeamsService)(&c.common)
298	c.Users = (*UsersService)(&c.common)
299	return c
300}
301
302// NewEnterpriseClient returns a new GitHub API client with provided
303// base URL and upload URL (often the same URL).
304// If either URL does not have a trailing slash, one is added automatically.
305// If a nil httpClient is provided, a new http.Client will be used.
306//
307// Note that NewEnterpriseClient is a convenience helper only;
308// its behavior is equivalent to using NewClient, followed by setting
309// the BaseURL and UploadURL fields.
310func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) {
311	baseEndpoint, err := url.Parse(baseURL)
312	if err != nil {
313		return nil, err
314	}
315	if !strings.HasSuffix(baseEndpoint.Path, "/") {
316		baseEndpoint.Path += "/"
317	}
318
319	uploadEndpoint, err := url.Parse(uploadURL)
320	if err != nil {
321		return nil, err
322	}
323	if !strings.HasSuffix(uploadEndpoint.Path, "/") {
324		uploadEndpoint.Path += "/"
325	}
326
327	c := NewClient(httpClient)
328	c.BaseURL = baseEndpoint
329	c.UploadURL = uploadEndpoint
330	return c, nil
331}
332
333// NewRequest creates an API request. A relative URL can be provided in urlStr,
334// in which case it is resolved relative to the BaseURL of the Client.
335// Relative URLs should always be specified without a preceding slash. If
336// specified, the value pointed to by body is JSON encoded and included as the
337// request body.
338func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
339	if !strings.HasSuffix(c.BaseURL.Path, "/") {
340		return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
341	}
342	u, err := c.BaseURL.Parse(urlStr)
343	if err != nil {
344		return nil, err
345	}
346
347	var buf io.ReadWriter
348	if body != nil {
349		buf = new(bytes.Buffer)
350		enc := json.NewEncoder(buf)
351		enc.SetEscapeHTML(false)
352		err := enc.Encode(body)
353		if err != nil {
354			return nil, err
355		}
356	}
357
358	req, err := http.NewRequest(method, u.String(), buf)
359	if err != nil {
360		return nil, err
361	}
362
363	if body != nil {
364		req.Header.Set("Content-Type", "application/json")
365	}
366	req.Header.Set("Accept", mediaTypeV3)
367	if c.UserAgent != "" {
368		req.Header.Set("User-Agent", c.UserAgent)
369	}
370	return req, nil
371}
372
373// NewUploadRequest creates an upload request. A relative URL can be provided in
374// urlStr, in which case it is resolved relative to the UploadURL of the Client.
375// Relative URLs should always be specified without a preceding slash.
376func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string) (*http.Request, error) {
377	if !strings.HasSuffix(c.UploadURL.Path, "/") {
378		return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL)
379	}
380	u, err := c.UploadURL.Parse(urlStr)
381	if err != nil {
382		return nil, err
383	}
384
385	req, err := http.NewRequest("POST", u.String(), reader)
386	if err != nil {
387		return nil, err
388	}
389	req.ContentLength = size
390
391	if mediaType == "" {
392		mediaType = defaultMediaType
393	}
394	req.Header.Set("Content-Type", mediaType)
395	req.Header.Set("Accept", mediaTypeV3)
396	req.Header.Set("User-Agent", c.UserAgent)
397	return req, nil
398}
399
400// Response is a GitHub API response. This wraps the standard http.Response
401// returned from GitHub and provides convenient access to things like
402// pagination links.
403type Response struct {
404	*http.Response
405
406	// These fields provide the page values for paginating through a set of
407	// results. Any or all of these may be set to the zero value for
408	// responses that are not part of a paginated set, or for which there
409	// are no additional pages.
410
411	NextPage  int
412	PrevPage  int
413	FirstPage int
414	LastPage  int
415
416	// Explicitly specify the Rate type so Rate's String() receiver doesn't
417	// propagate to Response.
418	Rate Rate
419}
420
421// newResponse creates a new Response for the provided http.Response.
422// r must not be nil.
423func newResponse(r *http.Response) *Response {
424	response := &Response{Response: r}
425	response.populatePageValues()
426	response.Rate = parseRate(r)
427	return response
428}
429
430// populatePageValues parses the HTTP Link response headers and populates the
431// various pagination link values in the Response.
432func (r *Response) populatePageValues() {
433	if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 {
434		for _, link := range strings.Split(links[0], ",") {
435			segments := strings.Split(strings.TrimSpace(link), ";")
436
437			// link must at least have href and rel
438			if len(segments) < 2 {
439				continue
440			}
441
442			// ensure href is properly formatted
443			if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") {
444				continue
445			}
446
447			// try to pull out page parameter
448			url, err := url.Parse(segments[0][1 : len(segments[0])-1])
449			if err != nil {
450				continue
451			}
452			page := url.Query().Get("page")
453			if page == "" {
454				continue
455			}
456
457			for _, segment := range segments[1:] {
458				switch strings.TrimSpace(segment) {
459				case `rel="next"`:
460					r.NextPage, _ = strconv.Atoi(page)
461				case `rel="prev"`:
462					r.PrevPage, _ = strconv.Atoi(page)
463				case `rel="first"`:
464					r.FirstPage, _ = strconv.Atoi(page)
465				case `rel="last"`:
466					r.LastPage, _ = strconv.Atoi(page)
467				}
468
469			}
470		}
471	}
472}
473
474// parseRate parses the rate related headers.
475func parseRate(r *http.Response) Rate {
476	var rate Rate
477	if limit := r.Header.Get(headerRateLimit); limit != "" {
478		rate.Limit, _ = strconv.Atoi(limit)
479	}
480	if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
481		rate.Remaining, _ = strconv.Atoi(remaining)
482	}
483	if reset := r.Header.Get(headerRateReset); reset != "" {
484		if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
485			rate.Reset = Timestamp{time.Unix(v, 0)}
486		}
487	}
488	return rate
489}
490
491// Do sends an API request and returns the API response. The API response is
492// JSON decoded and stored in the value pointed to by v, or returned as an
493// error if an API error has occurred. If v implements the io.Writer
494// interface, the raw response body will be written to v, without attempting to
495// first decode it. If rate limit is exceeded and reset time is in the future,
496// Do returns *RateLimitError immediately without making a network API call.
497//
498// The provided ctx must be non-nil. If it is canceled or times out,
499// ctx.Err() will be returned.
500func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
501	req = withContext(ctx, req)
502
503	rateLimitCategory := category(req.URL.Path)
504
505	// If we've hit rate limit, don't make further requests before Reset time.
506	if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil {
507		return &Response{
508			Response: err.Response,
509			Rate:     err.Rate,
510		}, err
511	}
512
513	resp, err := c.client.Do(req)
514	if err != nil {
515		// If we got an error, and the context has been canceled,
516		// the context's error is probably more useful.
517		select {
518		case <-ctx.Done():
519			return nil, ctx.Err()
520		default:
521		}
522
523		// If the error type is *url.Error, sanitize its URL before returning.
524		if e, ok := err.(*url.Error); ok {
525			if url, err := url.Parse(e.URL); err == nil {
526				e.URL = sanitizeURL(url).String()
527				return nil, e
528			}
529		}
530
531		return nil, err
532	}
533	defer resp.Body.Close()
534
535	response := newResponse(resp)
536
537	c.rateMu.Lock()
538	c.rateLimits[rateLimitCategory] = response.Rate
539	c.rateMu.Unlock()
540
541	err = CheckResponse(resp)
542	if err != nil {
543		// Special case for AcceptedErrors. If an AcceptedError
544		// has been encountered, the response's payload will be
545		// added to the AcceptedError and returned.
546		//
547		// Issue #1022
548		aerr, ok := err.(*AcceptedError)
549		if ok {
550			b, readErr := ioutil.ReadAll(resp.Body)
551			if readErr != nil {
552				return response, readErr
553			}
554
555			aerr.Raw = b
556			return response, aerr
557		}
558
559		return response, err
560	}
561
562	if v != nil {
563		if w, ok := v.(io.Writer); ok {
564			io.Copy(w, resp.Body)
565		} else {
566			decErr := json.NewDecoder(resp.Body).Decode(v)
567			if decErr == io.EOF {
568				decErr = nil // ignore EOF errors caused by empty response body
569			}
570			if decErr != nil {
571				err = decErr
572			}
573		}
574	}
575
576	return response, err
577}
578
579// checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from
580// current client state in order to quickly check if *RateLimitError can be immediately returned
581// from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily.
582// Otherwise it returns nil, and Client.Do should proceed normally.
583func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory rateLimitCategory) *RateLimitError {
584	c.rateMu.Lock()
585	rate := c.rateLimits[rateLimitCategory]
586	c.rateMu.Unlock()
587	if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) {
588		// Create a fake response.
589		resp := &http.Response{
590			Status:     http.StatusText(http.StatusForbidden),
591			StatusCode: http.StatusForbidden,
592			Request:    req,
593			Header:     make(http.Header),
594			Body:       ioutil.NopCloser(strings.NewReader("")),
595		}
596		return &RateLimitError{
597			Rate:     rate,
598			Response: resp,
599			Message:  fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time),
600		}
601	}
602
603	return nil
604}
605
606/*
607An ErrorResponse reports one or more errors caused by an API request.
608
609GitHub API docs: https://developer.github.com/v3/#client-errors
610*/
611type ErrorResponse struct {
612	Response *http.Response // HTTP response that caused this error
613	Message  string         `json:"message"` // error message
614	Errors   []Error        `json:"errors"`  // more detail on individual errors
615	// Block is only populated on certain types of errors such as code 451.
616	// See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/
617	// for more information.
618	Block *struct {
619		Reason    string     `json:"reason,omitempty"`
620		CreatedAt *Timestamp `json:"created_at,omitempty"`
621	} `json:"block,omitempty"`
622	// Most errors will also include a documentation_url field pointing
623	// to some content that might help you resolve the error, see
624	// https://developer.github.com/v3/#client-errors
625	DocumentationURL string `json:"documentation_url,omitempty"`
626}
627
628func (r *ErrorResponse) Error() string {
629	return fmt.Sprintf("%v %v: %d %v %+v",
630		r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
631		r.Response.StatusCode, r.Message, r.Errors)
632}
633
634// TwoFactorAuthError occurs when using HTTP Basic Authentication for a user
635// that has two-factor authentication enabled. The request can be reattempted
636// by providing a one-time password in the request.
637type TwoFactorAuthError ErrorResponse
638
639func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() }
640
641// RateLimitError occurs when GitHub returns 403 Forbidden response with a rate limit
642// remaining value of 0, and error message starts with "API rate limit exceeded for ".
643type RateLimitError struct {
644	Rate     Rate           // Rate specifies last known rate limit for the client
645	Response *http.Response // HTTP response that caused this error
646	Message  string         `json:"message"` // error message
647}
648
649func (r *RateLimitError) Error() string {
650	return fmt.Sprintf("%v %v: %d %v %v",
651		r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
652		r.Response.StatusCode, r.Message, formatRateReset(time.Until(r.Rate.Reset.Time)))
653}
654
655// AcceptedError occurs when GitHub returns 202 Accepted response with an
656// empty body, which means a job was scheduled on the GitHub side to process
657// the information needed and cache it.
658// Technically, 202 Accepted is not a real error, it's just used to
659// indicate that results are not ready yet, but should be available soon.
660// The request can be repeated after some time.
661type AcceptedError struct {
662	// Raw contains the response body.
663	Raw []byte
664}
665
666func (*AcceptedError) Error() string {
667	return "job scheduled on GitHub side; try again later"
668}
669
670// AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the
671// "documentation_url" field value equal to "https://developer.github.com/v3/#abuse-rate-limits".
672type AbuseRateLimitError struct {
673	Response *http.Response // HTTP response that caused this error
674	Message  string         `json:"message"` // error message
675
676	// RetryAfter is provided with some abuse rate limit errors. If present,
677	// it is the amount of time that the client should wait before retrying.
678	// Otherwise, the client should try again later (after an unspecified amount of time).
679	RetryAfter *time.Duration
680}
681
682func (r *AbuseRateLimitError) Error() string {
683	return fmt.Sprintf("%v %v: %d %v",
684		r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
685		r.Response.StatusCode, r.Message)
686}
687
688// sanitizeURL redacts the client_secret parameter from the URL which may be
689// exposed to the user.
690func sanitizeURL(uri *url.URL) *url.URL {
691	if uri == nil {
692		return nil
693	}
694	params := uri.Query()
695	if len(params.Get("client_secret")) > 0 {
696		params.Set("client_secret", "REDACTED")
697		uri.RawQuery = params.Encode()
698	}
699	return uri
700}
701
702/*
703An Error reports more details on an individual error in an ErrorResponse.
704These are the possible validation error codes:
705
706    missing:
707        resource does not exist
708    missing_field:
709        a required field on a resource has not been set
710    invalid:
711        the formatting of a field is invalid
712    already_exists:
713        another resource has the same valid as this field
714    custom:
715        some resources return this (e.g. github.User.CreateKey()), additional
716        information is set in the Message field of the Error
717
718GitHub API docs: https://developer.github.com/v3/#client-errors
719*/
720type Error struct {
721	Resource string `json:"resource"` // resource on which the error occurred
722	Field    string `json:"field"`    // field on which the error occurred
723	Code     string `json:"code"`     // validation error code
724	Message  string `json:"message"`  // Message describing the error. Errors with Code == "custom" will always have this set.
725}
726
727func (e *Error) Error() string {
728	return fmt.Sprintf("%v error caused by %v field on %v resource",
729		e.Code, e.Field, e.Resource)
730}
731
732// CheckResponse checks the API response for errors, and returns them if
733// present. A response is considered an error if it has a status code outside
734// the 200 range or equal to 202 Accepted.
735// API error responses are expected to have either no response
736// body, or a JSON response body that maps to ErrorResponse. Any other
737// response body will be silently ignored.
738//
739// The error type will be *RateLimitError for rate limit exceeded errors,
740// *AcceptedError for 202 Accepted status codes,
741// and *TwoFactorAuthError for two-factor authentication errors.
742func CheckResponse(r *http.Response) error {
743	if r.StatusCode == http.StatusAccepted {
744		return &AcceptedError{}
745	}
746	if c := r.StatusCode; 200 <= c && c <= 299 {
747		return nil
748	}
749	errorResponse := &ErrorResponse{Response: r}
750	data, err := ioutil.ReadAll(r.Body)
751	if err == nil && data != nil {
752		json.Unmarshal(data, errorResponse)
753	}
754	switch {
755	case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"):
756		return (*TwoFactorAuthError)(errorResponse)
757	case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0" && strings.HasPrefix(errorResponse.Message, "API rate limit exceeded for "):
758		return &RateLimitError{
759			Rate:     parseRate(r),
760			Response: errorResponse.Response,
761			Message:  errorResponse.Message,
762		}
763	case r.StatusCode == http.StatusForbidden && strings.HasSuffix(errorResponse.DocumentationURL, "/v3/#abuse-rate-limits"):
764		abuseRateLimitError := &AbuseRateLimitError{
765			Response: errorResponse.Response,
766			Message:  errorResponse.Message,
767		}
768		if v := r.Header["Retry-After"]; len(v) > 0 {
769			// According to GitHub support, the "Retry-After" header value will be
770			// an integer which represents the number of seconds that one should
771			// wait before resuming making requests.
772			retryAfterSeconds, _ := strconv.ParseInt(v[0], 10, 64) // Error handling is noop.
773			retryAfter := time.Duration(retryAfterSeconds) * time.Second
774			abuseRateLimitError.RetryAfter = &retryAfter
775		}
776		return abuseRateLimitError
777	default:
778		return errorResponse
779	}
780}
781
782// parseBoolResponse determines the boolean result from a GitHub API response.
783// Several GitHub API methods return boolean responses indicated by the HTTP
784// status code in the response (true indicated by a 204, false indicated by a
785// 404). This helper function will determine that result and hide the 404
786// error if present. Any other error will be returned through as-is.
787func parseBoolResponse(err error) (bool, error) {
788	if err == nil {
789		return true, nil
790	}
791
792	if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound {
793		// Simply false. In this one case, we do not pass the error through.
794		return false, nil
795	}
796
797	// some other real error occurred
798	return false, err
799}
800
801// Rate represents the rate limit for the current client.
802type Rate struct {
803	// The number of requests per hour the client is currently limited to.
804	Limit int `json:"limit"`
805
806	// The number of remaining requests the client can make this hour.
807	Remaining int `json:"remaining"`
808
809	// The time at which the current rate limit will reset.
810	Reset Timestamp `json:"reset"`
811}
812
813func (r Rate) String() string {
814	return Stringify(r)
815}
816
817// RateLimits represents the rate limits for the current client.
818type RateLimits struct {
819	// The rate limit for non-search API requests. Unauthenticated
820	// requests are limited to 60 per hour. Authenticated requests are
821	// limited to 5,000 per hour.
822	//
823	// GitHub API docs: https://developer.github.com/v3/#rate-limiting
824	Core *Rate `json:"core"`
825
826	// The rate limit for search API requests. Unauthenticated requests
827	// are limited to 10 requests per minutes. Authenticated requests are
828	// limited to 30 per minute.
829	//
830	// GitHub API docs: https://developer.github.com/v3/search/#rate-limit
831	Search *Rate `json:"search"`
832}
833
834func (r RateLimits) String() string {
835	return Stringify(r)
836}
837
838type rateLimitCategory uint8
839
840const (
841	coreCategory rateLimitCategory = iota
842	searchCategory
843
844	categories // An array of this length will be able to contain all rate limit categories.
845)
846
847// category returns the rate limit category of the endpoint, determined by Request.URL.Path.
848func category(path string) rateLimitCategory {
849	switch {
850	default:
851		return coreCategory
852	case strings.HasPrefix(path, "/search/"):
853		return searchCategory
854	}
855}
856
857// RateLimits returns the rate limits for the current client.
858func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) {
859	req, err := c.NewRequest("GET", "rate_limit", nil)
860	if err != nil {
861		return nil, nil, err
862	}
863
864	response := new(struct {
865		Resources *RateLimits `json:"resources"`
866	})
867	resp, err := c.Do(ctx, req, response)
868	if err != nil {
869		return nil, nil, err
870	}
871
872	if response.Resources != nil {
873		c.rateMu.Lock()
874		if response.Resources.Core != nil {
875			c.rateLimits[coreCategory] = *response.Resources.Core
876		}
877		if response.Resources.Search != nil {
878			c.rateLimits[searchCategory] = *response.Resources.Search
879		}
880		c.rateMu.Unlock()
881	}
882
883	return response.Resources, resp, nil
884}
885
886/*
887UnauthenticatedRateLimitedTransport allows you to make unauthenticated calls
888that need to use a higher rate limit associated with your OAuth application.
889
890	t := &github.UnauthenticatedRateLimitedTransport{
891		ClientID:     "your app's client ID",
892		ClientSecret: "your app's client secret",
893	}
894	client := github.NewClient(t.Client())
895
896This will append the querystring params client_id=xxx&client_secret=yyy to all
897requests.
898
899See https://developer.github.com/v3/#unauthenticated-rate-limited-requests for
900more information.
901*/
902type UnauthenticatedRateLimitedTransport struct {
903	// ClientID is the GitHub OAuth client ID of the current application, which
904	// can be found by selecting its entry in the list at
905	// https://github.com/settings/applications.
906	ClientID string
907
908	// ClientSecret is the GitHub OAuth client secret of the current
909	// application.
910	ClientSecret string
911
912	// Transport is the underlying HTTP transport to use when making requests.
913	// It will default to http.DefaultTransport if nil.
914	Transport http.RoundTripper
915}
916
917// RoundTrip implements the RoundTripper interface.
918func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
919	if t.ClientID == "" {
920		return nil, errors.New("t.ClientID is empty")
921	}
922	if t.ClientSecret == "" {
923		return nil, errors.New("t.ClientSecret is empty")
924	}
925
926	// To set extra querystring params, we must make a copy of the Request so
927	// that we don't modify the Request we were given. This is required by the
928	// specification of http.RoundTripper.
929	//
930	// Since we are going to modify only req.URL here, we only need a deep copy
931	// of req.URL.
932	req2 := new(http.Request)
933	*req2 = *req
934	req2.URL = new(url.URL)
935	*req2.URL = *req.URL
936
937	q := req2.URL.Query()
938	q.Set("client_id", t.ClientID)
939	q.Set("client_secret", t.ClientSecret)
940	req2.URL.RawQuery = q.Encode()
941
942	// Make the HTTP request.
943	return t.transport().RoundTrip(req2)
944}
945
946// Client returns an *http.Client that makes requests which are subject to the
947// rate limit of your OAuth application.
948func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client {
949	return &http.Client{Transport: t}
950}
951
952func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper {
953	if t.Transport != nil {
954		return t.Transport
955	}
956	return http.DefaultTransport
957}
958
959// BasicAuthTransport is an http.RoundTripper that authenticates all requests
960// using HTTP Basic Authentication with the provided username and password. It
961// additionally supports users who have two-factor authentication enabled on
962// their GitHub account.
963type BasicAuthTransport struct {
964	Username string // GitHub username
965	Password string // GitHub password
966	OTP      string // one-time password for users with two-factor auth enabled
967
968	// Transport is the underlying HTTP transport to use when making requests.
969	// It will default to http.DefaultTransport if nil.
970	Transport http.RoundTripper
971}
972
973// RoundTrip implements the RoundTripper interface.
974func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
975	// To set extra headers, we must make a copy of the Request so
976	// that we don't modify the Request we were given. This is required by the
977	// specification of http.RoundTripper.
978	//
979	// Since we are going to modify only req.Header here, we only need a deep copy
980	// of req.Header.
981	req2 := new(http.Request)
982	*req2 = *req
983	req2.Header = make(http.Header, len(req.Header))
984	for k, s := range req.Header {
985		req2.Header[k] = append([]string(nil), s...)
986	}
987
988	req2.SetBasicAuth(t.Username, t.Password)
989	if t.OTP != "" {
990		req2.Header.Set(headerOTP, t.OTP)
991	}
992	return t.transport().RoundTrip(req2)
993}
994
995// Client returns an *http.Client that makes requests that are authenticated
996// using HTTP Basic Authentication.
997func (t *BasicAuthTransport) Client() *http.Client {
998	return &http.Client{Transport: t}
999}
1000
1001func (t *BasicAuthTransport) transport() http.RoundTripper {
1002	if t.Transport != nil {
1003		return t.Transport
1004	}
1005	return http.DefaultTransport
1006}
1007
1008// formatRateReset formats d to look like "[rate reset in 2s]" or
1009// "[rate reset in 87m02s]" for the positive durations. And like "[rate limit was reset 87m02s ago]"
1010// for the negative cases.
1011func formatRateReset(d time.Duration) string {
1012	isNegative := d < 0
1013	if isNegative {
1014		d *= -1
1015	}
1016	secondsTotal := int(0.5 + d.Seconds())
1017	minutes := secondsTotal / 60
1018	seconds := secondsTotal - minutes*60
1019
1020	var timeString string
1021	if minutes > 0 {
1022		timeString = fmt.Sprintf("%dm%02ds", minutes, seconds)
1023	} else {
1024		timeString = fmt.Sprintf("%ds", seconds)
1025	}
1026
1027	if isNegative {
1028		return fmt.Sprintf("[rate limit was reset %v ago]", timeString)
1029	}
1030	return fmt.Sprintf("[rate reset in %v]", timeString)
1031}
1032
1033// Bool is a helper routine that allocates a new bool value
1034// to store v and returns a pointer to it.
1035func Bool(v bool) *bool { return &v }
1036
1037// Int is a helper routine that allocates a new int value
1038// to store v and returns a pointer to it.
1039func Int(v int) *int { return &v }
1040
1041// Int64 is a helper routine that allocates a new int64 value
1042// to store v and returns a pointer to it.
1043func Int64(v int64) *int64 { return &v }
1044
1045// String is a helper routine that allocates a new string value
1046// to store v and returns a pointer to it.
1047func String(v string) *string { return &v }
1048