// Copyright 2013 The go-github AUTHORS. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:generate go run gen-accessors.go //go:generate go run gen-stringify-test.go package github import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "reflect" "strconv" "strings" "sync" "time" "github.com/google/go-querystring/query" ) const ( defaultBaseURL = "https://api.github.com/" uploadBaseURL = "https://uploads.github.com/" userAgent = "go-github" headerRateLimit = "X-RateLimit-Limit" headerRateRemaining = "X-RateLimit-Remaining" headerRateReset = "X-RateLimit-Reset" headerOTP = "X-GitHub-OTP" mediaTypeV3 = "application/vnd.github.v3+json" defaultMediaType = "application/octet-stream" mediaTypeV3SHA = "application/vnd.github.v3.sha" mediaTypeV3Diff = "application/vnd.github.v3.diff" mediaTypeV3Patch = "application/vnd.github.v3.patch" mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json" // Media Type values to access preview APIs // https://developer.github.com/changes/2020-01-10-revoke-installation-token/ mediaTypeRevokeTokenPreview = "application/vnd.github.gambit-preview+json" // https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/ mediaTypeStarringPreview = "application/vnd.github.v3.star+json" // https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/ mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json" // https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/ mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json" // https://developer.github.com/changes/2018-10-16-deployments-environments-states-and-auto-inactive-updates/ mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json" // https://developer.github.com/changes/2016-05-12-reactions-api-preview/ mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview" // https://developer.github.com/changes/2016-05-23-timeline-preview-api/ mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json" // https://developer.github.com/changes/2016-09-14-projects-api/ mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json" // https://developer.github.com/changes/2016-09-14-Integrations-Early-Access/ mediaTypeIntegrationPreview = "application/vnd.github.machine-man-preview+json" // https://developer.github.com/changes/2017-01-05-commit-search-api/ mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json" // https://developer.github.com/changes/2017-02-28-user-blocking-apis-and-webhook/ mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json" // https://developer.github.com/changes/2017-02-09-community-health/ mediaTypeRepositoryCommunityHealthMetricsPreview = "application/vnd.github.black-panther-preview+json" // https://developer.github.com/changes/2017-05-23-coc-api/ mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json" // https://developer.github.com/changes/2017-07-17-update-topics-on-repositories/ mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json" // https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/ mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json" // https://developer.github.com/changes/2018-01-10-lock-reason-api-preview/ mediaTypeLockReasonPreview = "application/vnd.github.sailor-v-preview+json" // https://developer.github.com/changes/2018-05-07-new-checks-api-public-beta/ mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json" // https://developer.github.com/enterprise/2.13/v3/repos/pre_receive_hooks/ mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview" // https://developer.github.com/changes/2018-02-22-protected-branches-required-signatures/ mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json" // https://developer.github.com/changes/2018-09-05-project-card-events/ mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json" // https://developer.github.com/changes/2018-12-18-interactions-preview/ mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json" // https://developer.github.com/changes/2019-02-14-draft-pull-requests/ mediaTypeDraftPreview = "application/vnd.github.shadow-cat-preview+json" // https://developer.github.com/changes/2019-03-14-enabling-disabling-pages/ mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json" // https://developer.github.com/changes/2019-04-24-vulnerability-alerts/ mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json" // https://developer.github.com/changes/2019-06-04-automated-security-fixes/ mediaTypeRequiredAutomatedSecurityFixesPreview = "application/vnd.github.london-preview+json" // https://developer.github.com/changes/2019-05-29-update-branch-api/ mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json" // https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/ mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json" // https://developer.github.com/v3/previews/#repository-creation-permissions mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json" // https://developer.github.com/v3/previews/#create-and-use-repository-templates mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json" // https://developer.github.com/changes/2019-10-03-multi-line-comments/ mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json" ) // A Client manages communication with the GitHub API. type Client struct { clientMu sync.Mutex // clientMu protects the client during calls that modify the CheckRedirect func. client *http.Client // HTTP client used to communicate with the API. // Base URL for API requests. Defaults to the public GitHub API, but can be // set to a domain endpoint to use with GitHub Enterprise. BaseURL should // always be specified with a trailing slash. BaseURL *url.URL // Base URL for uploading files. UploadURL *url.URL // User agent used when communicating with the GitHub API. UserAgent string rateMu sync.Mutex rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls. common service // Reuse a single struct instead of allocating one for each service on the heap. // Services used for talking to different parts of the GitHub API. Actions *ActionsService Activity *ActivityService Admin *AdminService Apps *AppsService Authorizations *AuthorizationsService Checks *ChecksService Gists *GistsService Git *GitService Gitignores *GitignoresService Interactions *InteractionsService Issues *IssuesService Licenses *LicensesService Marketplace *MarketplaceService Migrations *MigrationService Organizations *OrganizationsService Projects *ProjectsService PullRequests *PullRequestsService Reactions *ReactionsService Repositories *RepositoriesService Search *SearchService Teams *TeamsService Users *UsersService } type service struct { client *Client } // ListOptions specifies the optional parameters to various List methods that // support offset pagination. type ListOptions struct { // For paginated result sets, page of results to retrieve. Page int `url:"page,omitempty"` // For paginated result sets, the number of results to include per page. PerPage int `url:"per_page,omitempty"` } // ListCursorOptions specifies the optional parameters to various List methods that // support cursor pagination. type ListCursorOptions struct { // For paginated result sets, page of results to retrieve. Page string `url:"page,omitempty"` // For paginated result sets, the number of results to include per page. PerPage int `url:"per_page,omitempty"` } // UploadOptions specifies the parameters to methods that support uploads. type UploadOptions struct { Name string `url:"name,omitempty"` Label string `url:"label,omitempty"` MediaType string `url:"-"` } // RawType represents type of raw format of a request instead of JSON. type RawType uint8 const ( // Diff format. Diff RawType = 1 + iota // Patch format. Patch ) // RawOptions specifies parameters when user wants to get raw format of // a response instead of JSON. type RawOptions struct { Type RawType } // addOptions adds the parameters in opt as URL query parameters to s. opt // must be a struct whose fields may contain "url" tags. func addOptions(s string, opts interface{}) (string, error) { v := reflect.ValueOf(opts) if v.Kind() == reflect.Ptr && v.IsNil() { return s, nil } u, err := url.Parse(s) if err != nil { return s, err } qs, err := query.Values(opts) if err != nil { return s, err } u.RawQuery = qs.Encode() return u.String(), nil } // NewClient returns a new GitHub API client. If a nil httpClient is // provided, a new http.Client will be used. To use API methods which require // authentication, provide an http.Client that will perform the authentication // for you (such as that provided by the golang.org/x/oauth2 library). func NewClient(httpClient *http.Client) *Client { if httpClient == nil { httpClient = &http.Client{} } baseURL, _ := url.Parse(defaultBaseURL) uploadURL, _ := url.Parse(uploadBaseURL) c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent, UploadURL: uploadURL} c.common.client = c c.Actions = (*ActionsService)(&c.common) c.Activity = (*ActivityService)(&c.common) c.Admin = (*AdminService)(&c.common) c.Apps = (*AppsService)(&c.common) c.Authorizations = (*AuthorizationsService)(&c.common) c.Checks = (*ChecksService)(&c.common) c.Gists = (*GistsService)(&c.common) c.Git = (*GitService)(&c.common) c.Gitignores = (*GitignoresService)(&c.common) c.Interactions = (*InteractionsService)(&c.common) c.Issues = (*IssuesService)(&c.common) c.Licenses = (*LicensesService)(&c.common) c.Marketplace = &MarketplaceService{client: c} c.Migrations = (*MigrationService)(&c.common) c.Organizations = (*OrganizationsService)(&c.common) c.Projects = (*ProjectsService)(&c.common) c.PullRequests = (*PullRequestsService)(&c.common) c.Reactions = (*ReactionsService)(&c.common) c.Repositories = (*RepositoriesService)(&c.common) c.Search = (*SearchService)(&c.common) c.Teams = (*TeamsService)(&c.common) c.Users = (*UsersService)(&c.common) return c } // NewEnterpriseClient returns a new GitHub API client with provided // base URL and upload URL (often the same URL and is your GitHub Enterprise hostname). // If either URL does not have the suffix "/api/v3/", it will be added automatically. // If a nil httpClient is provided, http.DefaultClient will be used. // // Note that NewEnterpriseClient is a convenience helper only; // its behavior is equivalent to using NewClient, followed by setting // the BaseURL and UploadURL fields. // // Another important thing is that by default, the GitHub Enterprise URL format // should be http(s)://[hostname]/api/v3 or you will always receive the 406 status code. func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) { baseEndpoint, err := url.Parse(baseURL) if err != nil { return nil, err } if !strings.HasSuffix(baseEndpoint.Path, "/") { baseEndpoint.Path += "/" } if !strings.HasSuffix(baseEndpoint.Path, "/api/v3/") { baseEndpoint.Path += "api/v3/" } uploadEndpoint, err := url.Parse(uploadURL) if err != nil { return nil, err } if !strings.HasSuffix(uploadEndpoint.Path, "/") { uploadEndpoint.Path += "/" } if !strings.HasSuffix(uploadEndpoint.Path, "/api/v3/") { uploadEndpoint.Path += "api/v3/" } c := NewClient(httpClient) c.BaseURL = baseEndpoint c.UploadURL = uploadEndpoint return c, nil } // NewRequest creates an API request. A relative URL can be provided in urlStr, // in which case it is resolved relative to the BaseURL of the Client. // Relative URLs should always be specified without a preceding slash. If // specified, the value pointed to by body is JSON encoded and included as the // request body. func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { if !strings.HasSuffix(c.BaseURL.Path, "/") { return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL) } u, err := c.BaseURL.Parse(urlStr) if err != nil { return nil, err } var buf io.ReadWriter if body != nil { buf = &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) err := enc.Encode(body) if err != nil { return nil, err } } req, err := http.NewRequest(method, u.String(), buf) if err != nil { return nil, err } if body != nil { req.Header.Set("Content-Type", "application/json") } req.Header.Set("Accept", mediaTypeV3) if c.UserAgent != "" { req.Header.Set("User-Agent", c.UserAgent) } return req, nil } // NewUploadRequest creates an upload request. A relative URL can be provided in // urlStr, in which case it is resolved relative to the UploadURL of the Client. // Relative URLs should always be specified without a preceding slash. func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string) (*http.Request, error) { if !strings.HasSuffix(c.UploadURL.Path, "/") { return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL) } u, err := c.UploadURL.Parse(urlStr) if err != nil { return nil, err } req, err := http.NewRequest("POST", u.String(), reader) if err != nil { return nil, err } req.ContentLength = size if mediaType == "" { mediaType = defaultMediaType } req.Header.Set("Content-Type", mediaType) req.Header.Set("Accept", mediaTypeV3) req.Header.Set("User-Agent", c.UserAgent) return req, nil } // Response is a GitHub API response. This wraps the standard http.Response // returned from GitHub and provides convenient access to things like // pagination links. type Response struct { *http.Response // These fields provide the page values for paginating through a set of // results. Any or all of these may be set to the zero value for // responses that are not part of a paginated set, or for which there // are no additional pages. // // These fields support what is called "offset pagination" and should // be used with the ListOptions struct. NextPage int PrevPage int FirstPage int LastPage int // Additionally, some APIs support "cursor pagination" instead of offset. // This means that a token points directly to the next record which // can lead to O(1) performance compared to O(n) performance provided // by offset pagination. // // For APIs that support cursor pagination (such as // TeamsService.ListIDPGroupsInOrganization), the following field // will be populated to point to the next page. // // To use this token, set ListCursorOptions.Page to this value before // calling the endpoint again. NextPageToken string // Explicitly specify the Rate type so Rate's String() receiver doesn't // propagate to Response. Rate Rate } // newResponse creates a new Response for the provided http.Response. // r must not be nil. func newResponse(r *http.Response) *Response { response := &Response{Response: r} response.populatePageValues() response.Rate = parseRate(r) return response } // populatePageValues parses the HTTP Link response headers and populates the // various pagination link values in the Response. func (r *Response) populatePageValues() { if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 { for _, link := range strings.Split(links[0], ",") { segments := strings.Split(strings.TrimSpace(link), ";") // link must at least have href and rel if len(segments) < 2 { continue } // ensure href is properly formatted if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") { continue } // try to pull out page parameter url, err := url.Parse(segments[0][1 : len(segments[0])-1]) if err != nil { continue } page := url.Query().Get("page") if page == "" { continue } for _, segment := range segments[1:] { switch strings.TrimSpace(segment) { case `rel="next"`: if r.NextPage, err = strconv.Atoi(page); err != nil { r.NextPageToken = page } case `rel="prev"`: r.PrevPage, _ = strconv.Atoi(page) case `rel="first"`: r.FirstPage, _ = strconv.Atoi(page) case `rel="last"`: r.LastPage, _ = strconv.Atoi(page) } } } } } // parseRate parses the rate related headers. func parseRate(r *http.Response) Rate { var rate Rate if limit := r.Header.Get(headerRateLimit); limit != "" { rate.Limit, _ = strconv.Atoi(limit) } if remaining := r.Header.Get(headerRateRemaining); remaining != "" { rate.Remaining, _ = strconv.Atoi(remaining) } if reset := r.Header.Get(headerRateReset); reset != "" { if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { rate.Reset = Timestamp{time.Unix(v, 0)} } } return rate } // Do sends an API request and returns the API response. The API response is // JSON decoded and stored in the value pointed to by v, or returned as an // error if an API error has occurred. If v implements the io.Writer // interface, the raw response body will be written to v, without attempting to // first decode it. If rate limit is exceeded and reset time is in the future, // Do returns *RateLimitError immediately without making a network API call. // // The provided ctx must be non-nil, if it is nil an error is returned. If it is canceled or times out, // ctx.Err() will be returned. func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) { if ctx == nil { return nil, errors.New("context must be non-nil") } req = withContext(ctx, req) rateLimitCategory := category(req.URL.Path) // If we've hit rate limit, don't make further requests before Reset time. if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil { return &Response{ Response: err.Response, Rate: err.Rate, }, err } resp, err := c.client.Do(req) if err != nil { // If we got an error, and the context has been canceled, // the context's error is probably more useful. select { case <-ctx.Done(): return nil, ctx.Err() default: } // If the error type is *url.Error, sanitize its URL before returning. if e, ok := err.(*url.Error); ok { if url, err := url.Parse(e.URL); err == nil { e.URL = sanitizeURL(url).String() return nil, e } } return nil, err } defer resp.Body.Close() response := newResponse(resp) c.rateMu.Lock() c.rateLimits[rateLimitCategory] = response.Rate c.rateMu.Unlock() err = CheckResponse(resp) if err != nil { // Special case for AcceptedErrors. If an AcceptedError // has been encountered, the response's payload will be // added to the AcceptedError and returned. // // Issue #1022 aerr, ok := err.(*AcceptedError) if ok { b, readErr := ioutil.ReadAll(resp.Body) if readErr != nil { return response, readErr } aerr.Raw = b return response, aerr } return response, err } if v != nil { if w, ok := v.(io.Writer); ok { io.Copy(w, resp.Body) } else { decErr := json.NewDecoder(resp.Body).Decode(v) if decErr == io.EOF { decErr = nil // ignore EOF errors caused by empty response body } if decErr != nil { err = decErr } } } return response, err } // checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from // current client state in order to quickly check if *RateLimitError can be immediately returned // from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. // Otherwise it returns nil, and Client.Do should proceed normally. func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory rateLimitCategory) *RateLimitError { c.rateMu.Lock() rate := c.rateLimits[rateLimitCategory] c.rateMu.Unlock() if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) { // Create a fake response. resp := &http.Response{ Status: http.StatusText(http.StatusForbidden), StatusCode: http.StatusForbidden, Request: req, Header: make(http.Header), Body: ioutil.NopCloser(strings.NewReader("")), } return &RateLimitError{ Rate: rate, Response: resp, Message: fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time), } } return nil } /* An ErrorResponse reports one or more errors caused by an API request. GitHub API docs: https://developer.github.com/v3/#client-errors */ type ErrorResponse struct { Response *http.Response // HTTP response that caused this error Message string `json:"message"` // error message Errors []Error `json:"errors"` // more detail on individual errors // Block is only populated on certain types of errors such as code 451. // See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/ // for more information. Block *struct { Reason string `json:"reason,omitempty"` CreatedAt *Timestamp `json:"created_at,omitempty"` } `json:"block,omitempty"` // Most errors will also include a documentation_url field pointing // to some content that might help you resolve the error, see // https://developer.github.com/v3/#client-errors DocumentationURL string `json:"documentation_url,omitempty"` } func (r *ErrorResponse) Error() string { return fmt.Sprintf("%v %v: %d %v %+v", r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), r.Response.StatusCode, r.Message, r.Errors) } // TwoFactorAuthError occurs when using HTTP Basic Authentication for a user // that has two-factor authentication enabled. The request can be reattempted // by providing a one-time password in the request. type TwoFactorAuthError ErrorResponse func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() } // RateLimitError occurs when GitHub returns 403 Forbidden response with a rate limit // remaining value of 0. type RateLimitError struct { Rate Rate // Rate specifies last known rate limit for the client Response *http.Response // HTTP response that caused this error Message string `json:"message"` // error message } func (r *RateLimitError) Error() string { return fmt.Sprintf("%v %v: %d %v %v", r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), r.Response.StatusCode, r.Message, formatRateReset(time.Until(r.Rate.Reset.Time))) } // AcceptedError occurs when GitHub returns 202 Accepted response with an // empty body, which means a job was scheduled on the GitHub side to process // the information needed and cache it. // Technically, 202 Accepted is not a real error, it's just used to // indicate that results are not ready yet, but should be available soon. // The request can be repeated after some time. type AcceptedError struct { // Raw contains the response body. Raw []byte } func (*AcceptedError) Error() string { return "job scheduled on GitHub side; try again later" } // AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the // "documentation_url" field value equal to "https://developer.github.com/v3/#abuse-rate-limits". type AbuseRateLimitError struct { Response *http.Response // HTTP response that caused this error Message string `json:"message"` // error message // RetryAfter is provided with some abuse rate limit errors. If present, // it is the amount of time that the client should wait before retrying. // Otherwise, the client should try again later (after an unspecified amount of time). RetryAfter *time.Duration } func (r *AbuseRateLimitError) Error() string { return fmt.Sprintf("%v %v: %d %v", r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), r.Response.StatusCode, r.Message) } // sanitizeURL redacts the client_secret parameter from the URL which may be // exposed to the user. func sanitizeURL(uri *url.URL) *url.URL { if uri == nil { return nil } params := uri.Query() if len(params.Get("client_secret")) > 0 { params.Set("client_secret", "REDACTED") uri.RawQuery = params.Encode() } return uri } /* An Error reports more details on an individual error in an ErrorResponse. These are the possible validation error codes: missing: resource does not exist missing_field: a required field on a resource has not been set invalid: the formatting of a field is invalid already_exists: another resource has the same valid as this field custom: some resources return this (e.g. github.User.CreateKey()), additional information is set in the Message field of the Error GitHub error responses structure are often undocumented and inconsistent. Sometimes error is just a simple string (Issue #540). In such cases, Message represents an error message as a workaround. GitHub API docs: https://developer.github.com/v3/#client-errors */ type Error struct { Resource string `json:"resource"` // resource on which the error occurred Field string `json:"field"` // field on which the error occurred Code string `json:"code"` // validation error code Message string `json:"message"` // Message describing the error. Errors with Code == "custom" will always have this set. } func (e *Error) Error() string { return fmt.Sprintf("%v error caused by %v field on %v resource", e.Code, e.Field, e.Resource) } func (e *Error) UnmarshalJSON(data []byte) error { type aliasError Error // avoid infinite recursion by using type alias. if err := json.Unmarshal(data, (*aliasError)(e)); err != nil { return json.Unmarshal(data, &e.Message) // data can be json string. } return nil } // CheckResponse checks the API response for errors, and returns them if // present. A response is considered an error if it has a status code outside // the 200 range or equal to 202 Accepted. // API error responses are expected to have response // body, and a JSON response body that maps to ErrorResponse. // // The error type will be *RateLimitError for rate limit exceeded errors, // *AcceptedError for 202 Accepted status codes, // and *TwoFactorAuthError for two-factor authentication errors. func CheckResponse(r *http.Response) error { if r.StatusCode == http.StatusAccepted { return &AcceptedError{} } if c := r.StatusCode; 200 <= c && c <= 299 { return nil } errorResponse := &ErrorResponse{Response: r} data, err := ioutil.ReadAll(r.Body) if err == nil && data != nil { json.Unmarshal(data, errorResponse) } // Re-populate error response body because GitHub error responses are often // undocumented and inconsistent. // Issue #1136, #540. r.Body = ioutil.NopCloser(bytes.NewBuffer(data)) switch { case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"): return (*TwoFactorAuthError)(errorResponse) case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0": return &RateLimitError{ Rate: parseRate(r), Response: errorResponse.Response, Message: errorResponse.Message, } case r.StatusCode == http.StatusForbidden && strings.HasSuffix(errorResponse.DocumentationURL, "/v3/#abuse-rate-limits"): abuseRateLimitError := &AbuseRateLimitError{ Response: errorResponse.Response, Message: errorResponse.Message, } if v := r.Header["Retry-After"]; len(v) > 0 { // According to GitHub support, the "Retry-After" header value will be // an integer which represents the number of seconds that one should // wait before resuming making requests. retryAfterSeconds, _ := strconv.ParseInt(v[0], 10, 64) // Error handling is noop. retryAfter := time.Duration(retryAfterSeconds) * time.Second abuseRateLimitError.RetryAfter = &retryAfter } return abuseRateLimitError default: return errorResponse } } // parseBoolResponse determines the boolean result from a GitHub API response. // Several GitHub API methods return boolean responses indicated by the HTTP // status code in the response (true indicated by a 204, false indicated by a // 404). This helper function will determine that result and hide the 404 // error if present. Any other error will be returned through as-is. func parseBoolResponse(err error) (bool, error) { if err == nil { return true, nil } if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound { // Simply false. In this one case, we do not pass the error through. return false, nil } // some other real error occurred return false, err } // Rate represents the rate limit for the current client. type Rate struct { // The number of requests per hour the client is currently limited to. Limit int `json:"limit"` // The number of remaining requests the client can make this hour. Remaining int `json:"remaining"` // The time at which the current rate limit will reset. Reset Timestamp `json:"reset"` } func (r Rate) String() string { return Stringify(r) } // RateLimits represents the rate limits for the current client. type RateLimits struct { // The rate limit for non-search API requests. Unauthenticated // requests are limited to 60 per hour. Authenticated requests are // limited to 5,000 per hour. // // GitHub API docs: https://developer.github.com/v3/#rate-limiting Core *Rate `json:"core"` // The rate limit for search API requests. Unauthenticated requests // are limited to 10 requests per minutes. Authenticated requests are // limited to 30 per minute. // // GitHub API docs: https://developer.github.com/v3/search/#rate-limit Search *Rate `json:"search"` } func (r RateLimits) String() string { return Stringify(r) } type rateLimitCategory uint8 const ( coreCategory rateLimitCategory = iota searchCategory categories // An array of this length will be able to contain all rate limit categories. ) // category returns the rate limit category of the endpoint, determined by Request.URL.Path. func category(path string) rateLimitCategory { switch { default: return coreCategory case strings.HasPrefix(path, "/search/"): return searchCategory } } // RateLimits returns the rate limits for the current client. func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) { req, err := c.NewRequest("GET", "rate_limit", nil) if err != nil { return nil, nil, err } response := new(struct { Resources *RateLimits `json:"resources"` }) resp, err := c.Do(ctx, req, response) if err != nil { return nil, nil, err } if response.Resources != nil { c.rateMu.Lock() if response.Resources.Core != nil { c.rateLimits[coreCategory] = *response.Resources.Core } if response.Resources.Search != nil { c.rateLimits[searchCategory] = *response.Resources.Search } c.rateMu.Unlock() } return response.Resources, resp, nil } /* UnauthenticatedRateLimitedTransport allows you to make unauthenticated calls that need to use a higher rate limit associated with your OAuth application. t := &github.UnauthenticatedRateLimitedTransport{ ClientID: "your app's client ID", ClientSecret: "your app's client secret", } client := github.NewClient(t.Client()) This will append the querystring params client_id=xxx&client_secret=yyy to all requests. See https://developer.github.com/v3/#unauthenticated-rate-limited-requests for more information. */ type UnauthenticatedRateLimitedTransport struct { // ClientID is the GitHub OAuth client ID of the current application, which // can be found by selecting its entry in the list at // https://github.com/settings/applications. ClientID string // ClientSecret is the GitHub OAuth client secret of the current // application. ClientSecret string // Transport is the underlying HTTP transport to use when making requests. // It will default to http.DefaultTransport if nil. Transport http.RoundTripper } // RoundTrip implements the RoundTripper interface. func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { if t.ClientID == "" { return nil, errors.New("t.ClientID is empty") } if t.ClientSecret == "" { return nil, errors.New("t.ClientSecret is empty") } // To set extra querystring params, we must make a copy of the Request so // that we don't modify the Request we were given. This is required by the // specification of http.RoundTripper. // // Since we are going to modify only req.URL here, we only need a deep copy // of req.URL. req2 := new(http.Request) *req2 = *req req2.URL = new(url.URL) *req2.URL = *req.URL q := req2.URL.Query() q.Set("client_id", t.ClientID) q.Set("client_secret", t.ClientSecret) req2.URL.RawQuery = q.Encode() // Make the HTTP request. return t.transport().RoundTrip(req2) } // Client returns an *http.Client that makes requests which are subject to the // rate limit of your OAuth application. func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client { return &http.Client{Transport: t} } func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper { if t.Transport != nil { return t.Transport } return http.DefaultTransport } // BasicAuthTransport is an http.RoundTripper that authenticates all requests // using HTTP Basic Authentication with the provided username and password. It // additionally supports users who have two-factor authentication enabled on // their GitHub account. type BasicAuthTransport struct { Username string // GitHub username Password string // GitHub password OTP string // one-time password for users with two-factor auth enabled // Transport is the underlying HTTP transport to use when making requests. // It will default to http.DefaultTransport if nil. Transport http.RoundTripper } // RoundTrip implements the RoundTripper interface. func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { // To set extra headers, we must make a copy of the Request so // that we don't modify the Request we were given. This is required by the // specification of http.RoundTripper. // // Since we are going to modify only req.Header here, we only need a deep copy // of req.Header. req2 := new(http.Request) *req2 = *req req2.Header = make(http.Header, len(req.Header)) for k, s := range req.Header { req2.Header[k] = append([]string(nil), s...) } req2.SetBasicAuth(t.Username, t.Password) if t.OTP != "" { req2.Header.Set(headerOTP, t.OTP) } return t.transport().RoundTrip(req2) } // Client returns an *http.Client that makes requests that are authenticated // using HTTP Basic Authentication. func (t *BasicAuthTransport) Client() *http.Client { return &http.Client{Transport: t} } func (t *BasicAuthTransport) transport() http.RoundTripper { if t.Transport != nil { return t.Transport } return http.DefaultTransport } // formatRateReset formats d to look like "[rate reset in 2s]" or // "[rate reset in 87m02s]" for the positive durations. And like "[rate limit was reset 87m02s ago]" // for the negative cases. func formatRateReset(d time.Duration) string { isNegative := d < 0 if isNegative { d *= -1 } secondsTotal := int(0.5 + d.Seconds()) minutes := secondsTotal / 60 seconds := secondsTotal - minutes*60 var timeString string if minutes > 0 { timeString = fmt.Sprintf("%dm%02ds", minutes, seconds) } else { timeString = fmt.Sprintf("%ds", seconds) } if isNegative { return fmt.Sprintf("[rate limit was reset %v ago]", timeString) } return fmt.Sprintf("[rate reset in %v]", timeString) } // Bool is a helper routine that allocates a new bool value // to store v and returns a pointer to it. func Bool(v bool) *bool { return &v } // Int is a helper routine that allocates a new int value // to store v and returns a pointer to it. func Int(v int) *int { return &v } // Int64 is a helper routine that allocates a new int64 value // to store v and returns a pointer to it. func Int64(v int64) *int64 { return &v } // String is a helper routine that allocates a new string value // to store v and returns a pointer to it. func String(v string) *string { return &v }