...

Source file src/github.com/google/go-github/v47/github/github.go

Documentation: github.com/google/go-github/v47/github

     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  
     9  package github
    10  
    11  import (
    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  
    30  const (
    31  	Version = "v47.0.0"
    32  
    33  	defaultBaseURL   = "https://api.github.com/"
    34  	defaultUserAgent = "go-github" + "/" + Version
    35  	uploadBaseURL    = "https://uploads.github.com/"
    36  
    37  	headerRateLimit     = "X-RateLimit-Limit"
    38  	headerRateRemaining = "X-RateLimit-Remaining"
    39  	headerRateReset     = "X-RateLimit-Reset"
    40  	headerOTP           = "X-GitHub-OTP"
    41  
    42  	headerTokenExpiration = "GitHub-Authentication-Token-Expiration"
    43  
    44  	mediaTypeV3                = "application/vnd.github.v3+json"
    45  	defaultMediaType           = "application/octet-stream"
    46  	mediaTypeV3SHA             = "application/vnd.github.v3.sha"
    47  	mediaTypeV3Diff            = "application/vnd.github.v3.diff"
    48  	mediaTypeV3Patch           = "application/vnd.github.v3.patch"
    49  	mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json"
    50  	mediaTypeIssueImportAPI    = "application/vnd.github.golden-comet-preview+json"
    51  
    52  	// Media Type values to access preview APIs
    53  	// These media types will be added to the API request as headers
    54  	// and used to enable particular features on GitHub API that are still in preview.
    55  	// After some time, specific media types will be promoted (to a "stable" state).
    56  	// From then on, the preview headers are not required anymore to activate the additional
    57  	// feature on GitHub.com's API. However, this API header might still be needed for users
    58  	// to run a GitHub Enterprise Server on-premise.
    59  	// It's not uncommon for GitHub Enterprise Server customers to run older versions which
    60  	// would probably rely on the preview headers for some time.
    61  	// While the header promotion is going out for GitHub.com, it may be some time before it
    62  	// even arrives in GitHub Enterprise Server.
    63  	// We keep those preview headers around to avoid breaking older GitHub Enterprise Server
    64  	// versions. Additionally, non-functional (preview) headers don't create any side effects
    65  	// on GitHub Cloud version.
    66  	//
    67  	// See https://github.com/google/go-github/pull/2125 for full context.
    68  
    69  	// https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/
    70  	mediaTypeStarringPreview = "application/vnd.github.v3.star+json"
    71  
    72  	// https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/
    73  	mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json"
    74  
    75  	// https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/
    76  	mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json"
    77  
    78  	// https://developer.github.com/changes/2018-10-16-deployments-environments-states-and-auto-inactive-updates/
    79  	mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json"
    80  
    81  	// https://developer.github.com/changes/2016-05-12-reactions-api-preview/
    82  	mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview"
    83  
    84  	// https://developer.github.com/changes/2016-05-23-timeline-preview-api/
    85  	mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json"
    86  
    87  	// https://developer.github.com/changes/2016-09-14-projects-api/
    88  	mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json"
    89  
    90  	// https://developer.github.com/changes/2017-01-05-commit-search-api/
    91  	mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json"
    92  
    93  	// https://developer.github.com/changes/2017-02-28-user-blocking-apis-and-webhook/
    94  	mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json"
    95  
    96  	// https://developer.github.com/changes/2017-05-23-coc-api/
    97  	mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json"
    98  
    99  	// https://developer.github.com/changes/2017-07-17-update-topics-on-repositories/
   100  	mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json"
   101  
   102  	// https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/
   103  	mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json"
   104  
   105  	// https://developer.github.com/changes/2018-05-07-new-checks-api-public-beta/
   106  	mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json"
   107  
   108  	// https://developer.github.com/enterprise/2.13/v3/repos/pre_receive_hooks/
   109  	mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview"
   110  
   111  	// https://developer.github.com/changes/2018-02-22-protected-branches-required-signatures/
   112  	mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json"
   113  
   114  	// https://developer.github.com/changes/2018-09-05-project-card-events/
   115  	mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json"
   116  
   117  	// https://developer.github.com/changes/2018-12-18-interactions-preview/
   118  	mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json"
   119  
   120  	// https://developer.github.com/changes/2019-03-14-enabling-disabling-pages/
   121  	mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json"
   122  
   123  	// https://developer.github.com/changes/2019-04-24-vulnerability-alerts/
   124  	mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json"
   125  
   126  	// https://developer.github.com/changes/2019-06-04-automated-security-fixes/
   127  	mediaTypeRequiredAutomatedSecurityFixesPreview = "application/vnd.github.london-preview+json"
   128  
   129  	// https://developer.github.com/changes/2019-05-29-update-branch-api/
   130  	mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json"
   131  
   132  	// https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/
   133  	mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json"
   134  
   135  	// https://docs.github.com/en/rest/previews/#repository-creation-permissions
   136  	mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json"
   137  
   138  	// https://docs.github.com/en/rest/previews/#create-and-use-repository-templates
   139  	mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json"
   140  
   141  	// https://developer.github.com/changes/2019-10-03-multi-line-comments/
   142  	mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json"
   143  
   144  	// https://developer.github.com/changes/2019-11-05-deprecated-passwords-and-authorizations-api/
   145  	mediaTypeOAuthAppPreview = "application/vnd.github.doctor-strange-preview+json"
   146  
   147  	// https://developer.github.com/changes/2019-12-03-internal-visibility-changes/
   148  	mediaTypeRepositoryVisibilityPreview = "application/vnd.github.nebula-preview+json"
   149  
   150  	// https://developer.github.com/changes/2018-12-10-content-attachments-api/
   151  	mediaTypeContentAttachmentsPreview = "application/vnd.github.corsair-preview+json"
   152  )
   153  
   154  var errNonNilContext = errors.New("context must be non-nil")
   155  
   156  // A Client manages communication with the GitHub API.
   157  type Client struct {
   158  	clientMu sync.Mutex   // clientMu protects the client during calls that modify the CheckRedirect func.
   159  	client   *http.Client // HTTP client used to communicate with the API.
   160  
   161  	// Base URL for API requests. Defaults to the public GitHub API, but can be
   162  	// set to a domain endpoint to use with GitHub Enterprise. BaseURL should
   163  	// always be specified with a trailing slash.
   164  	BaseURL *url.URL
   165  
   166  	// Base URL for uploading files.
   167  	UploadURL *url.URL
   168  
   169  	// User agent used when communicating with the GitHub API.
   170  	UserAgent string
   171  
   172  	rateMu     sync.Mutex
   173  	rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls.
   174  
   175  	common service // Reuse a single struct instead of allocating one for each service on the heap.
   176  
   177  	// Services used for talking to different parts of the GitHub API.
   178  	Actions        *ActionsService
   179  	Activity       *ActivityService
   180  	Admin          *AdminService
   181  	Apps           *AppsService
   182  	Authorizations *AuthorizationsService
   183  	Billing        *BillingService
   184  	Checks         *ChecksService
   185  	CodeScanning   *CodeScanningService
   186  	Dependabot     *DependabotService
   187  	Enterprise     *EnterpriseService
   188  	Gists          *GistsService
   189  	Git            *GitService
   190  	Gitignores     *GitignoresService
   191  	Interactions   *InteractionsService
   192  	IssueImport    *IssueImportService
   193  	Issues         *IssuesService
   194  	Licenses       *LicensesService
   195  	Marketplace    *MarketplaceService
   196  	Migrations     *MigrationService
   197  	Organizations  *OrganizationsService
   198  	Projects       *ProjectsService
   199  	PullRequests   *PullRequestsService
   200  	Reactions      *ReactionsService
   201  	Repositories   *RepositoriesService
   202  	SCIM           *SCIMService
   203  	Search         *SearchService
   204  	SecretScanning *SecretScanningService
   205  	Teams          *TeamsService
   206  	Users          *UsersService
   207  }
   208  
   209  type service struct {
   210  	client *Client
   211  }
   212  
   213  // Client returns the http.Client used by this GitHub client.
   214  func (c *Client) Client() *http.Client {
   215  	c.clientMu.Lock()
   216  	defer c.clientMu.Unlock()
   217  	clientCopy := *c.client
   218  	return &clientCopy
   219  }
   220  
   221  // ListOptions specifies the optional parameters to various List methods that
   222  // support offset pagination.
   223  type ListOptions struct {
   224  	// For paginated result sets, page of results to retrieve.
   225  	Page int `url:"page,omitempty"`
   226  
   227  	// For paginated result sets, the number of results to include per page.
   228  	PerPage int `url:"per_page,omitempty"`
   229  }
   230  
   231  // ListCursorOptions specifies the optional parameters to various List methods that
   232  // support cursor pagination.
   233  type ListCursorOptions struct {
   234  	// For paginated result sets, page of results to retrieve.
   235  	Page string `url:"page,omitempty"`
   236  
   237  	// For paginated result sets, the number of results to include per page.
   238  	PerPage int `url:"per_page,omitempty"`
   239  
   240  	// A cursor, as given in the Link header. If specified, the query only searches for events after this cursor.
   241  	After string `url:"after,omitempty"`
   242  
   243  	// A cursor, as given in the Link header. If specified, the query only searches for events before this cursor.
   244  	Before string `url:"before,omitempty"`
   245  
   246  	// A cursor, as given in the Link header. If specified, the query continues the search using this cursor.
   247  	Cursor string `url:"cursor,omitempty"`
   248  }
   249  
   250  // UploadOptions specifies the parameters to methods that support uploads.
   251  type UploadOptions struct {
   252  	Name      string `url:"name,omitempty"`
   253  	Label     string `url:"label,omitempty"`
   254  	MediaType string `url:"-"`
   255  }
   256  
   257  // RawType represents type of raw format of a request instead of JSON.
   258  type RawType uint8
   259  
   260  const (
   261  	// Diff format.
   262  	Diff RawType = 1 + iota
   263  	// Patch format.
   264  	Patch
   265  )
   266  
   267  // RawOptions specifies parameters when user wants to get raw format of
   268  // a response instead of JSON.
   269  type RawOptions struct {
   270  	Type RawType
   271  }
   272  
   273  // addOptions adds the parameters in opts as URL query parameters to s. opts
   274  // must be a struct whose fields may contain "url" tags.
   275  func addOptions(s string, opts interface{}) (string, error) {
   276  	v := reflect.ValueOf(opts)
   277  	if v.Kind() == reflect.Ptr && v.IsNil() {
   278  		return s, nil
   279  	}
   280  
   281  	u, err := url.Parse(s)
   282  	if err != nil {
   283  		return s, err
   284  	}
   285  
   286  	qs, err := query.Values(opts)
   287  	if err != nil {
   288  		return s, err
   289  	}
   290  
   291  	u.RawQuery = qs.Encode()
   292  	return u.String(), nil
   293  }
   294  
   295  // NewClient returns a new GitHub API client. If a nil httpClient is
   296  // provided, a new http.Client will be used. To use API methods which require
   297  // authentication, provide an http.Client that will perform the authentication
   298  // for you (such as that provided by the golang.org/x/oauth2 library).
   299  func NewClient(httpClient *http.Client) *Client {
   300  	if httpClient == nil {
   301  		httpClient = &http.Client{}
   302  	}
   303  	baseURL, _ := url.Parse(defaultBaseURL)
   304  	uploadURL, _ := url.Parse(uploadBaseURL)
   305  
   306  	c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: defaultUserAgent, UploadURL: uploadURL}
   307  	c.common.client = c
   308  	c.Actions = (*ActionsService)(&c.common)
   309  	c.Activity = (*ActivityService)(&c.common)
   310  	c.Admin = (*AdminService)(&c.common)
   311  	c.Apps = (*AppsService)(&c.common)
   312  	c.Authorizations = (*AuthorizationsService)(&c.common)
   313  	c.Billing = (*BillingService)(&c.common)
   314  	c.Checks = (*ChecksService)(&c.common)
   315  	c.CodeScanning = (*CodeScanningService)(&c.common)
   316  	c.Dependabot = (*DependabotService)(&c.common)
   317  	c.Enterprise = (*EnterpriseService)(&c.common)
   318  	c.Gists = (*GistsService)(&c.common)
   319  	c.Git = (*GitService)(&c.common)
   320  	c.Gitignores = (*GitignoresService)(&c.common)
   321  	c.Interactions = (*InteractionsService)(&c.common)
   322  	c.IssueImport = (*IssueImportService)(&c.common)
   323  	c.Issues = (*IssuesService)(&c.common)
   324  	c.Licenses = (*LicensesService)(&c.common)
   325  	c.Marketplace = &MarketplaceService{client: c}
   326  	c.Migrations = (*MigrationService)(&c.common)
   327  	c.Organizations = (*OrganizationsService)(&c.common)
   328  	c.Projects = (*ProjectsService)(&c.common)
   329  	c.PullRequests = (*PullRequestsService)(&c.common)
   330  	c.Reactions = (*ReactionsService)(&c.common)
   331  	c.Repositories = (*RepositoriesService)(&c.common)
   332  	c.SCIM = (*SCIMService)(&c.common)
   333  	c.Search = (*SearchService)(&c.common)
   334  	c.SecretScanning = (*SecretScanningService)(&c.common)
   335  	c.Teams = (*TeamsService)(&c.common)
   336  	c.Users = (*UsersService)(&c.common)
   337  	return c
   338  }
   339  
   340  // NewEnterpriseClient returns a new GitHub API client with provided
   341  // base URL and upload URL (often is your GitHub Enterprise hostname).
   342  // If the base URL does not have the suffix "/api/v3/", it will be added automatically.
   343  // If the upload URL does not have the suffix "/api/uploads", it will be added automatically.
   344  // If a nil httpClient is provided, a new http.Client will be used.
   345  //
   346  // Note that NewEnterpriseClient is a convenience helper only;
   347  // its behavior is equivalent to using NewClient, followed by setting
   348  // the BaseURL and UploadURL fields.
   349  //
   350  // Another important thing is that by default, the GitHub Enterprise URL format
   351  // should be http(s)://[hostname]/api/v3/ or you will always receive the 406 status code.
   352  // The upload URL format should be http(s)://[hostname]/api/uploads/.
   353  func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) {
   354  	baseEndpoint, err := url.Parse(baseURL)
   355  	if err != nil {
   356  		return nil, err
   357  	}
   358  
   359  	if !strings.HasSuffix(baseEndpoint.Path, "/") {
   360  		baseEndpoint.Path += "/"
   361  	}
   362  	if !strings.HasSuffix(baseEndpoint.Path, "/api/v3/") &&
   363  		!strings.HasPrefix(baseEndpoint.Host, "api.") &&
   364  		!strings.Contains(baseEndpoint.Host, ".api.") {
   365  		baseEndpoint.Path += "api/v3/"
   366  	}
   367  
   368  	uploadEndpoint, err := url.Parse(uploadURL)
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  
   373  	if !strings.HasSuffix(uploadEndpoint.Path, "/") {
   374  		uploadEndpoint.Path += "/"
   375  	}
   376  	if !strings.HasSuffix(uploadEndpoint.Path, "/api/uploads/") &&
   377  		!strings.HasPrefix(uploadEndpoint.Host, "api.") &&
   378  		!strings.Contains(uploadEndpoint.Host, ".api.") {
   379  		uploadEndpoint.Path += "api/uploads/"
   380  	}
   381  
   382  	c := NewClient(httpClient)
   383  	c.BaseURL = baseEndpoint
   384  	c.UploadURL = uploadEndpoint
   385  	return c, nil
   386  }
   387  
   388  // NewRequest creates an API request. A relative URL can be provided in urlStr,
   389  // in which case it is resolved relative to the BaseURL of the Client.
   390  // Relative URLs should always be specified without a preceding slash. If
   391  // specified, the value pointed to by body is JSON encoded and included as the
   392  // request body.
   393  func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
   394  	if !strings.HasSuffix(c.BaseURL.Path, "/") {
   395  		return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
   396  	}
   397  
   398  	u, err := c.BaseURL.Parse(urlStr)
   399  	if err != nil {
   400  		return nil, err
   401  	}
   402  
   403  	var buf io.ReadWriter
   404  	if body != nil {
   405  		buf = &bytes.Buffer{}
   406  		enc := json.NewEncoder(buf)
   407  		enc.SetEscapeHTML(false)
   408  		err := enc.Encode(body)
   409  		if err != nil {
   410  			return nil, err
   411  		}
   412  	}
   413  
   414  	req, err := http.NewRequest(method, u.String(), buf)
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  
   419  	if body != nil {
   420  		req.Header.Set("Content-Type", "application/json")
   421  	}
   422  	req.Header.Set("Accept", mediaTypeV3)
   423  	if c.UserAgent != "" {
   424  		req.Header.Set("User-Agent", c.UserAgent)
   425  	}
   426  	return req, nil
   427  }
   428  
   429  // NewFormRequest creates an API request. A relative URL can be provided in urlStr,
   430  // in which case it is resolved relative to the BaseURL of the Client.
   431  // Relative URLs should always be specified without a preceding slash.
   432  // Body is sent with Content-Type: application/x-www-form-urlencoded.
   433  func (c *Client) NewFormRequest(urlStr string, body io.Reader) (*http.Request, error) {
   434  	if !strings.HasSuffix(c.BaseURL.Path, "/") {
   435  		return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
   436  	}
   437  
   438  	u, err := c.BaseURL.Parse(urlStr)
   439  	if err != nil {
   440  		return nil, err
   441  	}
   442  
   443  	req, err := http.NewRequest(http.MethodPost, u.String(), body)
   444  	if err != nil {
   445  		return nil, err
   446  	}
   447  
   448  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   449  	req.Header.Set("Accept", mediaTypeV3)
   450  	if c.UserAgent != "" {
   451  		req.Header.Set("User-Agent", c.UserAgent)
   452  	}
   453  	return req, nil
   454  }
   455  
   456  // NewUploadRequest creates an upload request. A relative URL can be provided in
   457  // urlStr, in which case it is resolved relative to the UploadURL of the Client.
   458  // Relative URLs should always be specified without a preceding slash.
   459  func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string) (*http.Request, error) {
   460  	if !strings.HasSuffix(c.UploadURL.Path, "/") {
   461  		return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL)
   462  	}
   463  	u, err := c.UploadURL.Parse(urlStr)
   464  	if err != nil {
   465  		return nil, err
   466  	}
   467  
   468  	req, err := http.NewRequest("POST", u.String(), reader)
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  
   473  	req.ContentLength = size
   474  
   475  	if mediaType == "" {
   476  		mediaType = defaultMediaType
   477  	}
   478  	req.Header.Set("Content-Type", mediaType)
   479  	req.Header.Set("Accept", mediaTypeV3)
   480  	req.Header.Set("User-Agent", c.UserAgent)
   481  	return req, nil
   482  }
   483  
   484  // Response is a GitHub API response. This wraps the standard http.Response
   485  // returned from GitHub and provides convenient access to things like
   486  // pagination links.
   487  type Response struct {
   488  	*http.Response
   489  
   490  	// These fields provide the page values for paginating through a set of
   491  	// results. Any or all of these may be set to the zero value for
   492  	// responses that are not part of a paginated set, or for which there
   493  	// are no additional pages.
   494  	//
   495  	// These fields support what is called "offset pagination" and should
   496  	// be used with the ListOptions struct.
   497  	NextPage  int
   498  	PrevPage  int
   499  	FirstPage int
   500  	LastPage  int
   501  
   502  	// Additionally, some APIs support "cursor pagination" instead of offset.
   503  	// This means that a token points directly to the next record which
   504  	// can lead to O(1) performance compared to O(n) performance provided
   505  	// by offset pagination.
   506  	//
   507  	// For APIs that support cursor pagination (such as
   508  	// TeamsService.ListIDPGroupsInOrganization), the following field
   509  	// will be populated to point to the next page.
   510  	//
   511  	// To use this token, set ListCursorOptions.Page to this value before
   512  	// calling the endpoint again.
   513  	NextPageToken string
   514  
   515  	// For APIs that support cursor pagination, such as RepositoriesService.ListHookDeliveries,
   516  	// the following field will be populated to point to the next page.
   517  	// Set ListCursorOptions.Cursor to this value when calling the endpoint again.
   518  	Cursor string
   519  
   520  	// For APIs that support before/after pagination, such as OrganizationsService.AuditLog.
   521  	Before string
   522  	After  string
   523  
   524  	// Explicitly specify the Rate type so Rate's String() receiver doesn't
   525  	// propagate to Response.
   526  	Rate Rate
   527  
   528  	// token's expiration date
   529  	TokenExpiration Timestamp
   530  }
   531  
   532  // newResponse creates a new Response for the provided http.Response.
   533  // r must not be nil.
   534  func newResponse(r *http.Response) *Response {
   535  	response := &Response{Response: r}
   536  	response.populatePageValues()
   537  	response.Rate = parseRate(r)
   538  	response.TokenExpiration = parseTokenExpiration(r)
   539  	return response
   540  }
   541  
   542  // populatePageValues parses the HTTP Link response headers and populates the
   543  // various pagination link values in the Response.
   544  func (r *Response) populatePageValues() {
   545  	if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 {
   546  		for _, link := range strings.Split(links[0], ",") {
   547  			segments := strings.Split(strings.TrimSpace(link), ";")
   548  
   549  			// link must at least have href and rel
   550  			if len(segments) < 2 {
   551  				continue
   552  			}
   553  
   554  			// ensure href is properly formatted
   555  			if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") {
   556  				continue
   557  			}
   558  
   559  			// try to pull out page parameter
   560  			url, err := url.Parse(segments[0][1 : len(segments[0])-1])
   561  			if err != nil {
   562  				continue
   563  			}
   564  
   565  			q := url.Query()
   566  
   567  			if cursor := q.Get("cursor"); cursor != "" {
   568  				for _, segment := range segments[1:] {
   569  					switch strings.TrimSpace(segment) {
   570  					case `rel="next"`:
   571  						r.Cursor = cursor
   572  					}
   573  				}
   574  
   575  				continue
   576  			}
   577  
   578  			page := q.Get("page")
   579  			since := q.Get("since")
   580  			before := q.Get("before")
   581  			after := q.Get("after")
   582  
   583  			if page == "" && before == "" && after == "" && since == "" {
   584  				continue
   585  			}
   586  
   587  			if since != "" && page == "" {
   588  				page = since
   589  			}
   590  
   591  			for _, segment := range segments[1:] {
   592  				switch strings.TrimSpace(segment) {
   593  				case `rel="next"`:
   594  					if r.NextPage, err = strconv.Atoi(page); err != nil {
   595  						r.NextPageToken = page
   596  					}
   597  					r.After = after
   598  				case `rel="prev"`:
   599  					r.PrevPage, _ = strconv.Atoi(page)
   600  					r.Before = before
   601  				case `rel="first"`:
   602  					r.FirstPage, _ = strconv.Atoi(page)
   603  				case `rel="last"`:
   604  					r.LastPage, _ = strconv.Atoi(page)
   605  				}
   606  			}
   607  		}
   608  	}
   609  }
   610  
   611  // parseRate parses the rate related headers.
   612  func parseRate(r *http.Response) Rate {
   613  	var rate Rate
   614  	if limit := r.Header.Get(headerRateLimit); limit != "" {
   615  		rate.Limit, _ = strconv.Atoi(limit)
   616  	}
   617  	if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
   618  		rate.Remaining, _ = strconv.Atoi(remaining)
   619  	}
   620  	if reset := r.Header.Get(headerRateReset); reset != "" {
   621  		if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
   622  			rate.Reset = Timestamp{time.Unix(v, 0)}
   623  		}
   624  	}
   625  	return rate
   626  }
   627  
   628  // parseTokenExpiration parses the TokenExpiration related headers.
   629  func parseTokenExpiration(r *http.Response) Timestamp {
   630  	var exp Timestamp
   631  	if v := r.Header.Get(headerTokenExpiration); v != "" {
   632  		if t, err := time.Parse("2006-01-02 03:04:05 MST", v); err == nil {
   633  			exp = Timestamp{t.Local()}
   634  		}
   635  	}
   636  	return exp
   637  }
   638  
   639  type requestContext uint8
   640  
   641  const (
   642  	bypassRateLimitCheck requestContext = iota
   643  )
   644  
   645  // BareDo sends an API request and lets you handle the api response. If an error
   646  // or API Error occurs, the error will contain more information. Otherwise you
   647  // are supposed to read and close the response's Body. If rate limit is exceeded
   648  // and reset time is in the future, BareDo returns *RateLimitError immediately
   649  // without making a network API call.
   650  //
   651  // The provided ctx must be non-nil, if it is nil an error is returned. If it is
   652  // canceled or times out, ctx.Err() will be returned.
   653  func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, error) {
   654  	if ctx == nil {
   655  		return nil, errNonNilContext
   656  	}
   657  
   658  	req = withContext(ctx, req)
   659  
   660  	rateLimitCategory := category(req.URL.Path)
   661  
   662  	if bypass := ctx.Value(bypassRateLimitCheck); bypass == nil {
   663  		// If we've hit rate limit, don't make further requests before Reset time.
   664  		if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil {
   665  			return &Response{
   666  				Response: err.Response,
   667  				Rate:     err.Rate,
   668  			}, err
   669  		}
   670  	}
   671  
   672  	resp, err := c.client.Do(req)
   673  	if err != nil {
   674  		// If we got an error, and the context has been canceled,
   675  		// the context's error is probably more useful.
   676  		select {
   677  		case <-ctx.Done():
   678  			return nil, ctx.Err()
   679  		default:
   680  		}
   681  
   682  		// If the error type is *url.Error, sanitize its URL before returning.
   683  		if e, ok := err.(*url.Error); ok {
   684  			if url, err := url.Parse(e.URL); err == nil {
   685  				e.URL = sanitizeURL(url).String()
   686  				return nil, e
   687  			}
   688  		}
   689  
   690  		return nil, err
   691  	}
   692  
   693  	response := newResponse(resp)
   694  
   695  	// Don't update the rate limits if this was a cached response.
   696  	// X-From-Cache is set by https://github.com/gregjones/httpcache
   697  	if response.Header.Get("X-From-Cache") == "" {
   698  		c.rateMu.Lock()
   699  		c.rateLimits[rateLimitCategory] = response.Rate
   700  		c.rateMu.Unlock()
   701  	}
   702  
   703  	err = CheckResponse(resp)
   704  	if err != nil {
   705  		defer resp.Body.Close()
   706  		// Special case for AcceptedErrors. If an AcceptedError
   707  		// has been encountered, the response's payload will be
   708  		// added to the AcceptedError and returned.
   709  		//
   710  		// Issue #1022
   711  		aerr, ok := err.(*AcceptedError)
   712  		if ok {
   713  			b, readErr := ioutil.ReadAll(resp.Body)
   714  			if readErr != nil {
   715  				return response, readErr
   716  			}
   717  
   718  			aerr.Raw = b
   719  			err = aerr
   720  		}
   721  	}
   722  	return response, err
   723  }
   724  
   725  // Do sends an API request and returns the API response. The API response is
   726  // JSON decoded and stored in the value pointed to by v, or returned as an
   727  // error if an API error has occurred. If v implements the io.Writer interface,
   728  // the raw response body will be written to v, without attempting to first
   729  // decode it. If v is nil, and no error hapens, the response is returned as is.
   730  // If rate limit is exceeded and reset time is in the future, Do returns
   731  // *RateLimitError immediately without making a network API call.
   732  //
   733  // The provided ctx must be non-nil, if it is nil an error is returned. If it
   734  // is canceled or times out, ctx.Err() will be returned.
   735  func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
   736  	resp, err := c.BareDo(ctx, req)
   737  	if err != nil {
   738  		return resp, err
   739  	}
   740  	defer resp.Body.Close()
   741  
   742  	switch v := v.(type) {
   743  	case nil:
   744  	case io.Writer:
   745  		_, err = io.Copy(v, resp.Body)
   746  	default:
   747  		decErr := json.NewDecoder(resp.Body).Decode(v)
   748  		if decErr == io.EOF {
   749  			decErr = nil // ignore EOF errors caused by empty response body
   750  		}
   751  		if decErr != nil {
   752  			err = decErr
   753  		}
   754  	}
   755  	return resp, err
   756  }
   757  
   758  // checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from
   759  // current client state in order to quickly check if *RateLimitError can be immediately returned
   760  // from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily.
   761  // Otherwise it returns nil, and Client.Do should proceed normally.
   762  func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory rateLimitCategory) *RateLimitError {
   763  	c.rateMu.Lock()
   764  	rate := c.rateLimits[rateLimitCategory]
   765  	c.rateMu.Unlock()
   766  	if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) {
   767  		// Create a fake response.
   768  		resp := &http.Response{
   769  			Status:     http.StatusText(http.StatusForbidden),
   770  			StatusCode: http.StatusForbidden,
   771  			Request:    req,
   772  			Header:     make(http.Header),
   773  			Body:       ioutil.NopCloser(strings.NewReader("")),
   774  		}
   775  		return &RateLimitError{
   776  			Rate:     rate,
   777  			Response: resp,
   778  			Message:  fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time),
   779  		}
   780  	}
   781  
   782  	return nil
   783  }
   784  
   785  // compareHTTPResponse returns whether two http.Response objects are equal or not.
   786  // Currently, only StatusCode is checked. This function is used when implementing the
   787  // Is(error) bool interface for the custom error types in this package.
   788  func compareHTTPResponse(r1, r2 *http.Response) bool {
   789  	if r1 == nil && r2 == nil {
   790  		return true
   791  	}
   792  
   793  	if r1 != nil && r2 != nil {
   794  		return r1.StatusCode == r2.StatusCode
   795  	}
   796  	return false
   797  }
   798  
   799  /*
   800  An ErrorResponse reports one or more errors caused by an API request.
   801  
   802  GitHub API docs: https://docs.github.com/en/rest/#client-errors
   803  */
   804  type ErrorResponse struct {
   805  	Response *http.Response // HTTP response that caused this error
   806  	Message  string         `json:"message"` // error message
   807  	Errors   []Error        `json:"errors"`  // more detail on individual errors
   808  	// Block is only populated on certain types of errors such as code 451.
   809  	Block *ErrorBlock `json:"block,omitempty"`
   810  	// Most errors will also include a documentation_url field pointing
   811  	// to some content that might help you resolve the error, see
   812  	// https://docs.github.com/en/rest/#client-errors
   813  	DocumentationURL string `json:"documentation_url,omitempty"`
   814  }
   815  
   816  // ErrorBlock contains a further explanation for the reason of an error.
   817  // See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/
   818  // for more information.
   819  type ErrorBlock struct {
   820  	Reason    string     `json:"reason,omitempty"`
   821  	CreatedAt *Timestamp `json:"created_at,omitempty"`
   822  }
   823  
   824  func (r *ErrorResponse) Error() string {
   825  	return fmt.Sprintf("%v %v: %d %v %+v",
   826  		r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
   827  		r.Response.StatusCode, r.Message, r.Errors)
   828  }
   829  
   830  // Is returns whether the provided error equals this error.
   831  func (r *ErrorResponse) Is(target error) bool {
   832  	v, ok := target.(*ErrorResponse)
   833  	if !ok {
   834  		return false
   835  	}
   836  
   837  	if r.Message != v.Message || (r.DocumentationURL != v.DocumentationURL) ||
   838  		!compareHTTPResponse(r.Response, v.Response) {
   839  		return false
   840  	}
   841  
   842  	// Compare Errors.
   843  	if len(r.Errors) != len(v.Errors) {
   844  		return false
   845  	}
   846  	for idx := range r.Errors {
   847  		if r.Errors[idx] != v.Errors[idx] {
   848  			return false
   849  		}
   850  	}
   851  
   852  	// Compare Block.
   853  	if (r.Block != nil && v.Block == nil) || (r.Block == nil && v.Block != nil) {
   854  		return false
   855  	}
   856  	if r.Block != nil && v.Block != nil {
   857  		if r.Block.Reason != v.Block.Reason {
   858  			return false
   859  		}
   860  		if (r.Block.CreatedAt != nil && v.Block.CreatedAt == nil) || (r.Block.CreatedAt ==
   861  			nil && v.Block.CreatedAt != nil) {
   862  			return false
   863  		}
   864  		if r.Block.CreatedAt != nil && v.Block.CreatedAt != nil {
   865  			if *(r.Block.CreatedAt) != *(v.Block.CreatedAt) {
   866  				return false
   867  			}
   868  		}
   869  	}
   870  
   871  	return true
   872  }
   873  
   874  // TwoFactorAuthError occurs when using HTTP Basic Authentication for a user
   875  // that has two-factor authentication enabled. The request can be reattempted
   876  // by providing a one-time password in the request.
   877  type TwoFactorAuthError ErrorResponse
   878  
   879  func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() }
   880  
   881  // RateLimitError occurs when GitHub returns 403 Forbidden response with a rate limit
   882  // remaining value of 0.
   883  type RateLimitError struct {
   884  	Rate     Rate           // Rate specifies last known rate limit for the client
   885  	Response *http.Response // HTTP response that caused this error
   886  	Message  string         `json:"message"` // error message
   887  }
   888  
   889  func (r *RateLimitError) Error() string {
   890  	return fmt.Sprintf("%v %v: %d %v %v",
   891  		r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
   892  		r.Response.StatusCode, r.Message, formatRateReset(time.Until(r.Rate.Reset.Time)))
   893  }
   894  
   895  // Is returns whether the provided error equals this error.
   896  func (r *RateLimitError) Is(target error) bool {
   897  	v, ok := target.(*RateLimitError)
   898  	if !ok {
   899  		return false
   900  	}
   901  
   902  	return r.Rate == v.Rate &&
   903  		r.Message == v.Message &&
   904  		compareHTTPResponse(r.Response, v.Response)
   905  }
   906  
   907  // AcceptedError occurs when GitHub returns 202 Accepted response with an
   908  // empty body, which means a job was scheduled on the GitHub side to process
   909  // the information needed and cache it.
   910  // Technically, 202 Accepted is not a real error, it's just used to
   911  // indicate that results are not ready yet, but should be available soon.
   912  // The request can be repeated after some time.
   913  type AcceptedError struct {
   914  	// Raw contains the response body.
   915  	Raw []byte
   916  }
   917  
   918  func (*AcceptedError) Error() string {
   919  	return "job scheduled on GitHub side; try again later"
   920  }
   921  
   922  // Is returns whether the provided error equals this error.
   923  func (ae *AcceptedError) Is(target error) bool {
   924  	v, ok := target.(*AcceptedError)
   925  	if !ok {
   926  		return false
   927  	}
   928  	return bytes.Compare(ae.Raw, v.Raw) == 0
   929  }
   930  
   931  // AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the
   932  // "documentation_url" field value equal to "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits".
   933  type AbuseRateLimitError struct {
   934  	Response *http.Response // HTTP response that caused this error
   935  	Message  string         `json:"message"` // error message
   936  
   937  	// RetryAfter is provided with some abuse rate limit errors. If present,
   938  	// it is the amount of time that the client should wait before retrying.
   939  	// Otherwise, the client should try again later (after an unspecified amount of time).
   940  	RetryAfter *time.Duration
   941  }
   942  
   943  func (r *AbuseRateLimitError) Error() string {
   944  	return fmt.Sprintf("%v %v: %d %v",
   945  		r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
   946  		r.Response.StatusCode, r.Message)
   947  }
   948  
   949  // Is returns whether the provided error equals this error.
   950  func (r *AbuseRateLimitError) Is(target error) bool {
   951  	v, ok := target.(*AbuseRateLimitError)
   952  	if !ok {
   953  		return false
   954  	}
   955  
   956  	return r.Message == v.Message &&
   957  		r.RetryAfter == v.RetryAfter &&
   958  		compareHTTPResponse(r.Response, v.Response)
   959  }
   960  
   961  // sanitizeURL redacts the client_secret parameter from the URL which may be
   962  // exposed to the user.
   963  func sanitizeURL(uri *url.URL) *url.URL {
   964  	if uri == nil {
   965  		return nil
   966  	}
   967  	params := uri.Query()
   968  	if len(params.Get("client_secret")) > 0 {
   969  		params.Set("client_secret", "REDACTED")
   970  		uri.RawQuery = params.Encode()
   971  	}
   972  	return uri
   973  }
   974  
   975  /*
   976  An Error reports more details on an individual error in an ErrorResponse.
   977  These are the possible validation error codes:
   978  
   979  	missing:
   980  	    resource does not exist
   981  	missing_field:
   982  	    a required field on a resource has not been set
   983  	invalid:
   984  	    the formatting of a field is invalid
   985  	already_exists:
   986  	    another resource has the same valid as this field
   987  	custom:
   988  	    some resources return this (e.g. github.User.CreateKey()), additional
   989  	    information is set in the Message field of the Error
   990  
   991  GitHub error responses structure are often undocumented and inconsistent.
   992  Sometimes error is just a simple string (Issue #540).
   993  In such cases, Message represents an error message as a workaround.
   994  
   995  GitHub API docs: https://docs.github.com/en/rest/#client-errors
   996  */
   997  type Error struct {
   998  	Resource string `json:"resource"` // resource on which the error occurred
   999  	Field    string `json:"field"`    // field on which the error occurred
  1000  	Code     string `json:"code"`     // validation error code
  1001  	Message  string `json:"message"`  // Message describing the error. Errors with Code == "custom" will always have this set.
  1002  }
  1003  
  1004  func (e *Error) Error() string {
  1005  	return fmt.Sprintf("%v error caused by %v field on %v resource",
  1006  		e.Code, e.Field, e.Resource)
  1007  }
  1008  
  1009  func (e *Error) UnmarshalJSON(data []byte) error {
  1010  	type aliasError Error // avoid infinite recursion by using type alias.
  1011  	if err := json.Unmarshal(data, (*aliasError)(e)); err != nil {
  1012  		return json.Unmarshal(data, &e.Message) // data can be json string.
  1013  	}
  1014  	return nil
  1015  }
  1016  
  1017  // CheckResponse checks the API response for errors, and returns them if
  1018  // present. A response is considered an error if it has a status code outside
  1019  // the 200 range or equal to 202 Accepted.
  1020  // API error responses are expected to have response
  1021  // body, and a JSON response body that maps to ErrorResponse.
  1022  //
  1023  // The error type will be *RateLimitError for rate limit exceeded errors,
  1024  // *AcceptedError for 202 Accepted status codes,
  1025  // and *TwoFactorAuthError for two-factor authentication errors.
  1026  func CheckResponse(r *http.Response) error {
  1027  	if r.StatusCode == http.StatusAccepted {
  1028  		return &AcceptedError{}
  1029  	}
  1030  	if c := r.StatusCode; 200 <= c && c <= 299 {
  1031  		return nil
  1032  	}
  1033  
  1034  	errorResponse := &ErrorResponse{Response: r}
  1035  	data, err := ioutil.ReadAll(r.Body)
  1036  	if err == nil && data != nil {
  1037  		json.Unmarshal(data, errorResponse)
  1038  	}
  1039  	// Re-populate error response body because GitHub error responses are often
  1040  	// undocumented and inconsistent.
  1041  	// Issue #1136, #540.
  1042  	r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
  1043  	switch {
  1044  	case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"):
  1045  		return (*TwoFactorAuthError)(errorResponse)
  1046  	case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0":
  1047  		return &RateLimitError{
  1048  			Rate:     parseRate(r),
  1049  			Response: errorResponse.Response,
  1050  			Message:  errorResponse.Message,
  1051  		}
  1052  	case r.StatusCode == http.StatusForbidden &&
  1053  		(strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits") ||
  1054  			strings.HasSuffix(errorResponse.DocumentationURL, "#secondary-rate-limits")):
  1055  		abuseRateLimitError := &AbuseRateLimitError{
  1056  			Response: errorResponse.Response,
  1057  			Message:  errorResponse.Message,
  1058  		}
  1059  		if v := r.Header["Retry-After"]; len(v) > 0 {
  1060  			// According to GitHub support, the "Retry-After" header value will be
  1061  			// an integer which represents the number of seconds that one should
  1062  			// wait before resuming making requests.
  1063  			retryAfterSeconds, _ := strconv.ParseInt(v[0], 10, 64) // Error handling is noop.
  1064  			retryAfter := time.Duration(retryAfterSeconds) * time.Second
  1065  			abuseRateLimitError.RetryAfter = &retryAfter
  1066  		}
  1067  		return abuseRateLimitError
  1068  	default:
  1069  		return errorResponse
  1070  	}
  1071  }
  1072  
  1073  // parseBoolResponse determines the boolean result from a GitHub API response.
  1074  // Several GitHub API methods return boolean responses indicated by the HTTP
  1075  // status code in the response (true indicated by a 204, false indicated by a
  1076  // 404). This helper function will determine that result and hide the 404
  1077  // error if present. Any other error will be returned through as-is.
  1078  func parseBoolResponse(err error) (bool, error) {
  1079  	if err == nil {
  1080  		return true, nil
  1081  	}
  1082  
  1083  	if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound {
  1084  		// Simply false. In this one case, we do not pass the error through.
  1085  		return false, nil
  1086  	}
  1087  
  1088  	// some other real error occurred
  1089  	return false, err
  1090  }
  1091  
  1092  // Rate represents the rate limit for the current client.
  1093  type Rate struct {
  1094  	// The number of requests per hour the client is currently limited to.
  1095  	Limit int `json:"limit"`
  1096  
  1097  	// The number of remaining requests the client can make this hour.
  1098  	Remaining int `json:"remaining"`
  1099  
  1100  	// The time at which the current rate limit will reset.
  1101  	Reset Timestamp `json:"reset"`
  1102  }
  1103  
  1104  func (r Rate) String() string {
  1105  	return Stringify(r)
  1106  }
  1107  
  1108  // RateLimits represents the rate limits for the current client.
  1109  type RateLimits struct {
  1110  	// The rate limit for non-search API requests. Unauthenticated
  1111  	// requests are limited to 60 per hour. Authenticated requests are
  1112  	// limited to 5,000 per hour.
  1113  	//
  1114  	// GitHub API docs: https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
  1115  	Core *Rate `json:"core"`
  1116  
  1117  	// The rate limit for search API requests. Unauthenticated requests
  1118  	// are limited to 10 requests per minutes. Authenticated requests are
  1119  	// limited to 30 per minute.
  1120  	//
  1121  	// GitHub API docs: https://docs.github.com/en/rest/search#rate-limit
  1122  	Search *Rate `json:"search"`
  1123  
  1124  	// GitHub API docs: https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit
  1125  	GraphQL *Rate `json:"graphql"`
  1126  
  1127  	// GitHub API dos: https://docs.github.com/en/rest/rate-limit
  1128  	IntegrationManifest *Rate `json:"integration_manifest"`
  1129  
  1130  	SourceImport              *Rate `json:"source_import"`
  1131  	CodeScanningUpload        *Rate `json:"code_scanning_upload"`
  1132  	ActionsRunnerRegistration *Rate `json:"actions_runner_registration"`
  1133  	SCIM                      *Rate `json:"scim"`
  1134  }
  1135  
  1136  func (r RateLimits) String() string {
  1137  	return Stringify(r)
  1138  }
  1139  
  1140  type rateLimitCategory uint8
  1141  
  1142  const (
  1143  	coreCategory rateLimitCategory = iota
  1144  	searchCategory
  1145  	graphqlCategory
  1146  	integrationManifestCategory
  1147  	sourceImportCategory
  1148  	codeScanningUploadCategory
  1149  	actionsRunnerRegistrationCategory
  1150  	scimCategory
  1151  
  1152  	categories // An array of this length will be able to contain all rate limit categories.
  1153  )
  1154  
  1155  // category returns the rate limit category of the endpoint, determined by Request.URL.Path.
  1156  func category(path string) rateLimitCategory {
  1157  	switch {
  1158  	default:
  1159  		return coreCategory
  1160  	case strings.HasPrefix(path, "/search/"):
  1161  		return searchCategory
  1162  	}
  1163  }
  1164  
  1165  // RateLimits returns the rate limits for the current client.
  1166  func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) {
  1167  	req, err := c.NewRequest("GET", "rate_limit", nil)
  1168  	if err != nil {
  1169  		return nil, nil, err
  1170  	}
  1171  
  1172  	response := new(struct {
  1173  		Resources *RateLimits `json:"resources"`
  1174  	})
  1175  
  1176  	// This resource is not subject to rate limits.
  1177  	ctx = context.WithValue(ctx, bypassRateLimitCheck, true)
  1178  	resp, err := c.Do(ctx, req, response)
  1179  	if err != nil {
  1180  		return nil, resp, err
  1181  	}
  1182  
  1183  	if response.Resources != nil {
  1184  		c.rateMu.Lock()
  1185  		if response.Resources.Core != nil {
  1186  			c.rateLimits[coreCategory] = *response.Resources.Core
  1187  		}
  1188  		if response.Resources.Search != nil {
  1189  			c.rateLimits[searchCategory] = *response.Resources.Search
  1190  		}
  1191  		if response.Resources.GraphQL != nil {
  1192  			c.rateLimits[graphqlCategory] = *response.Resources.GraphQL
  1193  		}
  1194  		if response.Resources.IntegrationManifest != nil {
  1195  			c.rateLimits[integrationManifestCategory] = *response.Resources.IntegrationManifest
  1196  		}
  1197  		if response.Resources.SourceImport != nil {
  1198  			c.rateLimits[sourceImportCategory] = *response.Resources.SourceImport
  1199  		}
  1200  		if response.Resources.CodeScanningUpload != nil {
  1201  			c.rateLimits[codeScanningUploadCategory] = *response.Resources.CodeScanningUpload
  1202  		}
  1203  		if response.Resources.ActionsRunnerRegistration != nil {
  1204  			c.rateLimits[actionsRunnerRegistrationCategory] = *response.Resources.ActionsRunnerRegistration
  1205  		}
  1206  		if response.Resources.SCIM != nil {
  1207  			c.rateLimits[scimCategory] = *response.Resources.SCIM
  1208  		}
  1209  		c.rateMu.Unlock()
  1210  	}
  1211  
  1212  	return response.Resources, resp, nil
  1213  }
  1214  
  1215  func setCredentialsAsHeaders(req *http.Request, id, secret string) *http.Request {
  1216  	// To set extra headers, we must make a copy of the Request so
  1217  	// that we don't modify the Request we were given. This is required by the
  1218  	// specification of http.RoundTripper.
  1219  	//
  1220  	// Since we are going to modify only req.Header here, we only need a deep copy
  1221  	// of req.Header.
  1222  	convertedRequest := new(http.Request)
  1223  	*convertedRequest = *req
  1224  	convertedRequest.Header = make(http.Header, len(req.Header))
  1225  
  1226  	for k, s := range req.Header {
  1227  		convertedRequest.Header[k] = append([]string(nil), s...)
  1228  	}
  1229  	convertedRequest.SetBasicAuth(id, secret)
  1230  	return convertedRequest
  1231  }
  1232  
  1233  /*
  1234  UnauthenticatedRateLimitedTransport allows you to make unauthenticated calls
  1235  that need to use a higher rate limit associated with your OAuth application.
  1236  
  1237  	t := &github.UnauthenticatedRateLimitedTransport{
  1238  		ClientID:     "your app's client ID",
  1239  		ClientSecret: "your app's client secret",
  1240  	}
  1241  	client := github.NewClient(t.Client())
  1242  
  1243  This will add the client id and secret as a base64-encoded string in the format
  1244  ClientID:ClientSecret and apply it as an "Authorization": "Basic" header.
  1245  
  1246  See https://docs.github.com/en/rest/#unauthenticated-rate-limited-requests for
  1247  more information.
  1248  */
  1249  type UnauthenticatedRateLimitedTransport struct {
  1250  	// ClientID is the GitHub OAuth client ID of the current application, which
  1251  	// can be found by selecting its entry in the list at
  1252  	// https://github.com/settings/applications.
  1253  	ClientID string
  1254  
  1255  	// ClientSecret is the GitHub OAuth client secret of the current
  1256  	// application.
  1257  	ClientSecret string
  1258  
  1259  	// Transport is the underlying HTTP transport to use when making requests.
  1260  	// It will default to http.DefaultTransport if nil.
  1261  	Transport http.RoundTripper
  1262  }
  1263  
  1264  // RoundTrip implements the RoundTripper interface.
  1265  func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  1266  	if t.ClientID == "" {
  1267  		return nil, errors.New("t.ClientID is empty")
  1268  	}
  1269  	if t.ClientSecret == "" {
  1270  		return nil, errors.New("t.ClientSecret is empty")
  1271  	}
  1272  
  1273  	req2 := setCredentialsAsHeaders(req, t.ClientID, t.ClientSecret)
  1274  	// Make the HTTP request.
  1275  	return t.transport().RoundTrip(req2)
  1276  }
  1277  
  1278  // Client returns an *http.Client that makes requests which are subject to the
  1279  // rate limit of your OAuth application.
  1280  func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client {
  1281  	return &http.Client{Transport: t}
  1282  }
  1283  
  1284  func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper {
  1285  	if t.Transport != nil {
  1286  		return t.Transport
  1287  	}
  1288  	return http.DefaultTransport
  1289  }
  1290  
  1291  // BasicAuthTransport is an http.RoundTripper that authenticates all requests
  1292  // using HTTP Basic Authentication with the provided username and password. It
  1293  // additionally supports users who have two-factor authentication enabled on
  1294  // their GitHub account.
  1295  type BasicAuthTransport struct {
  1296  	Username string // GitHub username
  1297  	Password string // GitHub password
  1298  	OTP      string // one-time password for users with two-factor auth enabled
  1299  
  1300  	// Transport is the underlying HTTP transport to use when making requests.
  1301  	// It will default to http.DefaultTransport if nil.
  1302  	Transport http.RoundTripper
  1303  }
  1304  
  1305  // RoundTrip implements the RoundTripper interface.
  1306  func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  1307  	req2 := setCredentialsAsHeaders(req, t.Username, t.Password)
  1308  	if t.OTP != "" {
  1309  		req2.Header.Set(headerOTP, t.OTP)
  1310  	}
  1311  	return t.transport().RoundTrip(req2)
  1312  }
  1313  
  1314  // Client returns an *http.Client that makes requests that are authenticated
  1315  // using HTTP Basic Authentication.
  1316  func (t *BasicAuthTransport) Client() *http.Client {
  1317  	return &http.Client{Transport: t}
  1318  }
  1319  
  1320  func (t *BasicAuthTransport) transport() http.RoundTripper {
  1321  	if t.Transport != nil {
  1322  		return t.Transport
  1323  	}
  1324  	return http.DefaultTransport
  1325  }
  1326  
  1327  // formatRateReset formats d to look like "[rate reset in 2s]" or
  1328  // "[rate reset in 87m02s]" for the positive durations. And like "[rate limit was reset 87m02s ago]"
  1329  // for the negative cases.
  1330  func formatRateReset(d time.Duration) string {
  1331  	isNegative := d < 0
  1332  	if isNegative {
  1333  		d *= -1
  1334  	}
  1335  	secondsTotal := int(0.5 + d.Seconds())
  1336  	minutes := secondsTotal / 60
  1337  	seconds := secondsTotal - minutes*60
  1338  
  1339  	var timeString string
  1340  	if minutes > 0 {
  1341  		timeString = fmt.Sprintf("%dm%02ds", minutes, seconds)
  1342  	} else {
  1343  		timeString = fmt.Sprintf("%ds", seconds)
  1344  	}
  1345  
  1346  	if isNegative {
  1347  		return fmt.Sprintf("[rate limit was reset %v ago]", timeString)
  1348  	}
  1349  	return fmt.Sprintf("[rate reset in %v]", timeString)
  1350  }
  1351  
  1352  // When using roundTripWithOptionalFollowRedirect, note that it
  1353  // is the responsibility of the caller to close the response body.
  1354  func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
  1355  	req, err := c.NewRequest("GET", u, nil)
  1356  	if err != nil {
  1357  		return nil, err
  1358  	}
  1359  
  1360  	var resp *http.Response
  1361  	// Use http.DefaultTransport if no custom Transport is configured
  1362  	req = withContext(ctx, req)
  1363  	if c.client.Transport == nil {
  1364  		resp, err = http.DefaultTransport.RoundTrip(req)
  1365  	} else {
  1366  		resp, err = c.client.Transport.RoundTrip(req)
  1367  	}
  1368  	if err != nil {
  1369  		return nil, err
  1370  	}
  1371  
  1372  	// If redirect response is returned, follow it
  1373  	if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
  1374  		resp.Body.Close()
  1375  		u = resp.Header.Get("Location")
  1376  		resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, false)
  1377  	}
  1378  	return resp, err
  1379  }
  1380  
  1381  // Bool is a helper routine that allocates a new bool value
  1382  // to store v and returns a pointer to it.
  1383  func Bool(v bool) *bool { return &v }
  1384  
  1385  // Int is a helper routine that allocates a new int value
  1386  // to store v and returns a pointer to it.
  1387  func Int(v int) *int { return &v }
  1388  
  1389  // Int64 is a helper routine that allocates a new int64 value
  1390  // to store v and returns a pointer to it.
  1391  func Int64(v int64) *int64 { return &v }
  1392  
  1393  // String is a helper routine that allocates a new string value
  1394  // to store v and returns a pointer to it.
  1395  func String(v string) *string { return &v }
  1396  

View as plain text