aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/google/go-github/v39/github/github.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/google/go-github/v39/github/github.go')
-rw-r--r--vendor/github.com/google/go-github/v39/github/github.go1254
1 files changed, 1254 insertions, 0 deletions
diff --git a/vendor/github.com/google/go-github/v39/github/github.go b/vendor/github.com/google/go-github/v39/github/github.go
new file mode 100644
index 0000000000..d4d7a54a7f
--- /dev/null
+++ b/vendor/github.com/google/go-github/v39/github/github.go
@@ -0,0 +1,1254 @@
+// 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"
+
+ headerTokenExpiration = "GitHub-Authentication-Token-Expiration"
+
+ 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"
+ mediaTypeIssueImportAPI = "application/vnd.github.golden-comet-preview+json"
+
+ // Media Type values to access preview APIs
+
+ // 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/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-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/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-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://docs.github.com/en/free-pro-team@latest/rest/reference/previews/#repository-creation-permissions
+ mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json"
+
+ // https://docs.github.com/en/free-pro-team@latest/rest/reference/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"
+
+ // https://developer.github.com/changes/2019-11-05-deprecated-passwords-and-authorizations-api/
+ mediaTypeOAuthAppPreview = "application/vnd.github.doctor-strange-preview+json"
+
+ // https://developer.github.com/changes/2019-12-03-internal-visibility-changes/
+ mediaTypeRepositoryVisibilityPreview = "application/vnd.github.nebula-preview+json"
+
+ // https://developer.github.com/changes/2018-12-10-content-attachments-api/
+ mediaTypeContentAttachmentsPreview = "application/vnd.github.corsair-preview+json"
+)
+
+var errNonNilContext = errors.New("context must be non-nil")
+
+// 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
+ Billing *BillingService
+ Checks *ChecksService
+ CodeScanning *CodeScanningService
+ Enterprise *EnterpriseService
+ Gists *GistsService
+ Git *GitService
+ Gitignores *GitignoresService
+ Interactions *InteractionsService
+ IssueImport *IssueImportService
+ Issues *IssuesService
+ Licenses *LicensesService
+ Marketplace *MarketplaceService
+ Migrations *MigrationService
+ Organizations *OrganizationsService
+ Projects *ProjectsService
+ PullRequests *PullRequestsService
+ Reactions *ReactionsService
+ Repositories *RepositoriesService
+ SCIM *SCIMService
+ Search *SearchService
+ Teams *TeamsService
+ Users *UsersService
+}
+
+type service struct {
+ client *Client
+}
+
+// Client returns the http.Client used by this GitHub client.
+func (c *Client) Client() *http.Client {
+ c.clientMu.Lock()
+ defer c.clientMu.Unlock()
+ clientCopy := *c.client
+ return &clientCopy
+}
+
+// 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"`
+
+ // A cursor, as given in the Link header. If specified, the query only searches for events after this cursor.
+ After string `url:"after,omitempty"`
+
+ // A cursor, as given in the Link header. If specified, the query only searches for events before this cursor.
+ Before string `url:"before,omitempty"`
+
+ // A cursor, as given in the Link header. If specified, the query continues the search using this cursor.
+ Cursor string `url:"cursor,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 opts as URL query parameters to s. opts
+// 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.Billing = (*BillingService)(&c.common)
+ c.Checks = (*ChecksService)(&c.common)
+ c.CodeScanning = (*CodeScanningService)(&c.common)
+ c.Enterprise = (*EnterpriseService)(&c.common)
+ c.Gists = (*GistsService)(&c.common)
+ c.Git = (*GitService)(&c.common)
+ c.Gitignores = (*GitignoresService)(&c.common)
+ c.Interactions = (*InteractionsService)(&c.common)
+ c.IssueImport = (*IssueImportService)(&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.SCIM = (*SCIMService)(&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 is your GitHub Enterprise hostname).
+// If the base URL does not have the suffix "/api/v3/", it will be added automatically.
+// If the upload URL does not have the suffix "/api/uploads", it will be added automatically.
+// If a nil httpClient is provided, a new http.Client 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.
+// The upload URL format should be http(s)://[hostname]/api/uploads/.
+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/") &&
+ !strings.HasPrefix(baseEndpoint.Host, "api.") &&
+ !strings.Contains(baseEndpoint.Host, ".api.") {
+ 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/uploads/") &&
+ !strings.HasPrefix(uploadEndpoint.Host, "api.") &&
+ !strings.Contains(uploadEndpoint.Host, ".api.") {
+ uploadEndpoint.Path += "api/uploads/"
+ }
+
+ 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
+
+ // For APIs that support cursor pagination, such as RepositoriesService.ListHookDeliveries,
+ // the following field will be populated to point to the next page.
+ // Set ListCursorOptions.Cursor to this value when calling the endpoint again.
+ Cursor string
+
+ // Explicitly specify the Rate type so Rate's String() receiver doesn't
+ // propagate to Response.
+ Rate Rate
+
+ // token's expiration date
+ TokenExpiration Timestamp
+}
+
+// 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)
+ response.TokenExpiration = parseTokenExpiration(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
+ }
+
+ q := url.Query()
+
+ if cursor := q.Get("cursor"); cursor != "" {
+ for _, segment := range segments[1:] {
+ switch strings.TrimSpace(segment) {
+ case `rel="next"`:
+ r.Cursor = cursor
+ }
+ }
+
+ continue
+ }
+
+ page := q.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
+}
+
+// parseTokenExpiration parses the TokenExpiration related headers.
+func parseTokenExpiration(r *http.Response) Timestamp {
+ var exp Timestamp
+ if v := r.Header.Get(headerTokenExpiration); v != "" {
+ if t, err := time.Parse("2006-01-02 03:04:05 MST", v); err == nil {
+ exp = Timestamp{t.Local()}
+ }
+ }
+ return exp
+}
+
+type requestContext uint8
+
+const (
+ bypassRateLimitCheck requestContext = iota
+)
+
+// BareDo sends an API request and lets you handle the api response. If an error
+// or API Error occurs, the error will contain more information. Otherwise you
+// are supposed to read and close the response's Body. If rate limit is exceeded
+// and reset time is in the future, BareDo 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) BareDo(ctx context.Context, req *http.Request) (*Response, error) {
+ if ctx == nil {
+ return nil, errNonNilContext
+ }
+ req = withContext(ctx, req)
+
+ rateLimitCategory := category(req.URL.Path)
+
+ if bypass := ctx.Value(bypassRateLimitCheck); bypass == nil {
+ // 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
+ }
+
+ response := newResponse(resp)
+
+ c.rateMu.Lock()
+ c.rateLimits[rateLimitCategory] = response.Rate
+ c.rateMu.Unlock()
+
+ err = CheckResponse(resp)
+ if err != nil {
+ defer resp.Body.Close()
+ // 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
+ err = aerr
+ }
+ }
+ return response, err
+}
+
+// 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 v is nil, and no error hapens, the response is returned as is.
+// 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) {
+ resp, err := c.BareDo(ctx, req)
+ if err != nil {
+ return resp, err
+ }
+ defer resp.Body.Close()
+
+ switch v := v.(type) {
+ case nil:
+ case io.Writer:
+ _, err = io.Copy(v, resp.Body)
+ default:
+ 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 resp, 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
+}
+
+// compareHTTPResponse returns whether two http.Response objects are equal or not.
+// Currently, only StatusCode is checked. This function is used when implementing the
+// Is(error) bool interface for the custom error types in this package.
+func compareHTTPResponse(r1, r2 *http.Response) bool {
+ if r1 == nil && r2 == nil {
+ return true
+ }
+
+ if r1 != nil && r2 != nil {
+ return r1.StatusCode == r2.StatusCode
+ }
+ return false
+}
+
+/*
+An ErrorResponse reports one or more errors caused by an API request.
+
+GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/#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.
+ Block *ErrorBlock `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://docs.github.com/en/free-pro-team@latest/rest/reference/#client-errors
+ DocumentationURL string `json:"documentation_url,omitempty"`
+}
+
+// ErrorBlock contains a further explanation for the reason of an error.
+// See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/
+// for more information.
+type ErrorBlock struct {
+ Reason string `json:"reason,omitempty"`
+ CreatedAt *Timestamp `json:"created_at,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)
+}
+
+// Is returns whether the provided error equals this error.
+func (r *ErrorResponse) Is(target error) bool {
+ v, ok := target.(*ErrorResponse)
+ if !ok {
+ return false
+ }
+
+ if r.Message != v.Message || (r.DocumentationURL != v.DocumentationURL) ||
+ !compareHTTPResponse(r.Response, v.Response) {
+ return false
+ }
+
+ // Compare Errors.
+ if len(r.Errors) != len(v.Errors) {
+ return false
+ }
+ for idx := range r.Errors {
+ if r.Errors[idx] != v.Errors[idx] {
+ return false
+ }
+ }
+
+ // Compare Block.
+ if (r.Block != nil && v.Block == nil) || (r.Block == nil && v.Block != nil) {
+ return false
+ }
+ if r.Block != nil && v.Block != nil {
+ if r.Block.Reason != v.Block.Reason {
+ return false
+ }
+ if (r.Block.CreatedAt != nil && v.Block.CreatedAt == nil) || (r.Block.CreatedAt ==
+ nil && v.Block.CreatedAt != nil) {
+ return false
+ }
+ if r.Block.CreatedAt != nil && v.Block.CreatedAt != nil {
+ if *(r.Block.CreatedAt) != *(v.Block.CreatedAt) {
+ return false
+ }
+ }
+ }
+
+ return true
+}
+
+// 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)))
+}
+
+// Is returns whether the provided error equals this error.
+func (r *RateLimitError) Is(target error) bool {
+ v, ok := target.(*RateLimitError)
+ if !ok {
+ return false
+ }
+
+ return r.Rate == v.Rate &&
+ r.Message == v.Message &&
+ compareHTTPResponse(r.Response, v.Response)
+}
+
+// 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"
+}
+
+// Is returns whether the provided error equals this error.
+func (ae *AcceptedError) Is(target error) bool {
+ v, ok := target.(*AcceptedError)
+ if !ok {
+ return false
+ }
+ return bytes.Compare(ae.Raw, v.Raw) == 0
+}
+
+// AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the
+// "documentation_url" field value equal to "https://docs.github.com/en/free-pro-team@latest/rest/reference/#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)
+}
+
+// Is returns whether the provided error equals this error.
+func (r *AbuseRateLimitError) Is(target error) bool {
+ v, ok := target.(*AbuseRateLimitError)
+ if !ok {
+ return false
+ }
+
+ return r.Message == v.Message &&
+ r.RetryAfter == v.RetryAfter &&
+ compareHTTPResponse(r.Response, v.Response)
+}
+
+// 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://docs.github.com/en/free-pro-team@latest/rest/reference/#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, "#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://docs.github.com/en/free-pro-team@latest/rest/reference/#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://docs.github.com/en/free-pro-team@latest/rest/reference/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"`
+ })
+
+ // This resource is not subject to rate limits.
+ ctx = context.WithValue(ctx, bypassRateLimitCheck, true)
+ resp, err := c.Do(ctx, req, response)
+ if err != nil {
+ return nil, resp, 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
+}
+
+func setCredentialsAsHeaders(req *http.Request, id, secret string) *http.Request {
+ // 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.
+ convertedRequest := new(http.Request)
+ *convertedRequest = *req
+ convertedRequest.Header = make(http.Header, len(req.Header))
+
+ for k, s := range req.Header {
+ convertedRequest.Header[k] = append([]string(nil), s...)
+ }
+ convertedRequest.SetBasicAuth(id, secret)
+ return convertedRequest
+}
+
+/*
+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 add the client id and secret as a base64-encoded string in the format
+ClientID:ClientSecret and apply it as an "Authorization": "Basic" header.
+
+See https://docs.github.com/en/free-pro-team@latest/rest/reference/#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")
+ }
+
+ req2 := setCredentialsAsHeaders(req, t.ClientID, t.ClientSecret)
+ // 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) {
+ req2 := setCredentialsAsHeaders(req, 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 }