...

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

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

View as plain text