github.com/google/go-github/v69@v69.2.0/github/github.go (about)

     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  //go:generate ../script/metadata.sh update-go
     9  
    10  package github
    11  
    12  import (
    13  	"bytes"
    14  	"context"
    15  	"encoding/json"
    16  	"errors"
    17  	"fmt"
    18  	"io"
    19  	"net/http"
    20  	"net/url"
    21  	"reflect"
    22  	"regexp"
    23  	"strconv"
    24  	"strings"
    25  	"sync"
    26  	"time"
    27  
    28  	"github.com/google/go-querystring/query"
    29  )
    30  
    31  const (
    32  	Version = "v69.2.0"
    33  
    34  	defaultAPIVersion = "2022-11-28"
    35  	defaultBaseURL    = "https://api.github.com/"
    36  	defaultUserAgent  = "go-github" + "/" + Version
    37  	uploadBaseURL     = "https://uploads.github.com/"
    38  
    39  	headerAPIVersion    = "X-Github-Api-Version"
    40  	headerRateLimit     = "X-Ratelimit-Limit"
    41  	headerRateRemaining = "X-Ratelimit-Remaining"
    42  	headerRateUsed      = "X-Ratelimit-Used"
    43  	headerRateReset     = "X-Ratelimit-Reset"
    44  	headerRateResource  = "X-Ratelimit-Resource"
    45  	headerOTP           = "X-Github-Otp"
    46  	headerRetryAfter    = "Retry-After"
    47  
    48  	headerTokenExpiration = "Github-Authentication-Token-Expiration"
    49  
    50  	mediaTypeV3                = "application/vnd.github.v3+json"
    51  	defaultMediaType           = "application/octet-stream"
    52  	mediaTypeV3SHA             = "application/vnd.github.v3.sha"
    53  	mediaTypeV3Diff            = "application/vnd.github.v3.diff"
    54  	mediaTypeV3Patch           = "application/vnd.github.v3.patch"
    55  	mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json"
    56  	mediaTypeIssueImportAPI    = "application/vnd.github.golden-comet-preview+json"
    57  
    58  	// Media Type values to access preview APIs
    59  	// These media types will be added to the API request as headers
    60  	// and used to enable particular features on GitHub API that are still in preview.
    61  	// After some time, specific media types will be promoted (to a "stable" state).
    62  	// From then on, the preview headers are not required anymore to activate the additional
    63  	// feature on GitHub.com's API. However, this API header might still be needed for users
    64  	// to run a GitHub Enterprise Server on-premise.
    65  	// It's not uncommon for GitHub Enterprise Server customers to run older versions which
    66  	// would probably rely on the preview headers for some time.
    67  	// While the header promotion is going out for GitHub.com, it may be some time before it
    68  	// even arrives in GitHub Enterprise Server.
    69  	// We keep those preview headers around to avoid breaking older GitHub Enterprise Server
    70  	// versions. Additionally, non-functional (preview) headers don't create any side effects
    71  	// on GitHub Cloud version.
    72  	//
    73  	// See https://github.com/google/go-github/pull/2125 for full context.
    74  
    75  	// https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/
    76  	mediaTypeStarringPreview = "application/vnd.github.v3.star+json"
    77  
    78  	// https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/
    79  	mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json"
    80  
    81  	// https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/
    82  	mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json"
    83  
    84  	// https://developer.github.com/changes/2018-10-16-deployments-environments-states-and-auto-inactive-updates/
    85  	mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json"
    86  
    87  	// https://developer.github.com/changes/2016-05-12-reactions-api-preview/
    88  	mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview"
    89  
    90  	// https://developer.github.com/changes/2016-05-23-timeline-preview-api/
    91  	mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json"
    92  
    93  	// https://developer.github.com/changes/2016-09-14-projects-api/
    94  	mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json"
    95  
    96  	// https://developer.github.com/changes/2017-01-05-commit-search-api/
    97  	mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json"
    98  
    99  	// https://developer.github.com/changes/2017-02-28-user-blocking-apis-and-webhook/
   100  	mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json"
   101  
   102  	// https://developer.github.com/changes/2017-05-23-coc-api/
   103  	mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json"
   104  
   105  	// https://developer.github.com/changes/2017-07-17-update-topics-on-repositories/
   106  	mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json"
   107  
   108  	// https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/
   109  	mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json"
   110  
   111  	// https://developer.github.com/changes/2018-05-07-new-checks-api-public-beta/
   112  	mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json"
   113  
   114  	// https://developer.github.com/enterprise/2.13/v3/repos/pre_receive_hooks/
   115  	mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview"
   116  
   117  	// https://developer.github.com/changes/2018-02-22-protected-branches-required-signatures/
   118  	mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json"
   119  
   120  	// https://developer.github.com/changes/2018-09-05-project-card-events/
   121  	mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json"
   122  
   123  	// https://developer.github.com/changes/2018-12-18-interactions-preview/
   124  	mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json"
   125  
   126  	// https://developer.github.com/changes/2019-03-14-enabling-disabling-pages/
   127  	mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json"
   128  
   129  	// https://developer.github.com/changes/2019-04-24-vulnerability-alerts/
   130  	mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json"
   131  
   132  	// https://developer.github.com/changes/2019-05-29-update-branch-api/
   133  	mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json"
   134  
   135  	// https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/
   136  	mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json"
   137  
   138  	// https://docs.github.com/rest/previews/#repository-creation-permissions
   139  	mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json"
   140  
   141  	// https://docs.github.com/rest/previews/#create-and-use-repository-templates
   142  	mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json"
   143  
   144  	// https://developer.github.com/changes/2019-10-03-multi-line-comments/
   145  	mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json"
   146  
   147  	// https://developer.github.com/changes/2019-11-05-deprecated-passwords-and-authorizations-api/
   148  	mediaTypeOAuthAppPreview = "application/vnd.github.doctor-strange-preview+json"
   149  
   150  	// https://developer.github.com/changes/2019-12-03-internal-visibility-changes/
   151  	mediaTypeRepositoryVisibilityPreview = "application/vnd.github.nebula-preview+json"
   152  
   153  	// https://developer.github.com/changes/2018-12-10-content-attachments-api/
   154  	mediaTypeContentAttachmentsPreview = "application/vnd.github.corsair-preview+json"
   155  )
   156  
   157  var errNonNilContext = errors.New("context must be non-nil")
   158  
   159  // A Client manages communication with the GitHub API.
   160  type Client struct {
   161  	clientMu              sync.Mutex   // clientMu protects the client during calls that modify the CheckRedirect func.
   162  	client                *http.Client // HTTP client used to communicate with the API.
   163  	clientIgnoreRedirects *http.Client // HTTP client used to communicate with the API on endpoints where we don't want to follow redirects.
   164  
   165  	// Base URL for API requests. Defaults to the public GitHub API, but can be
   166  	// set to a domain endpoint to use with GitHub Enterprise. BaseURL should
   167  	// always be specified with a trailing slash.
   168  	BaseURL *url.URL
   169  
   170  	// Base URL for uploading files.
   171  	UploadURL *url.URL
   172  
   173  	// User agent used when communicating with the GitHub API.
   174  	UserAgent string
   175  
   176  	rateMu                  sync.Mutex
   177  	rateLimits              [Categories]Rate // Rate limits for the client as determined by the most recent API calls.
   178  	secondaryRateLimitReset time.Time        // Secondary rate limit reset for the client as determined by the most recent API calls.
   179  
   180  	// If specified, Client will block requests for at most this duration in case of reaching a secondary
   181  	// rate limit
   182  	MaxSecondaryRateLimitRetryAfterDuration time.Duration
   183  
   184  	// Whether to respect rate limit headers on endpoints that return 302 redirections to artifacts
   185  	RateLimitRedirectionalEndpoints bool
   186  
   187  	common service // Reuse a single struct instead of allocating one for each service on the heap.
   188  
   189  	// Services used for talking to different parts of the GitHub API.
   190  	Actions            *ActionsService
   191  	Activity           *ActivityService
   192  	Admin              *AdminService
   193  	Apps               *AppsService
   194  	Authorizations     *AuthorizationsService
   195  	Billing            *BillingService
   196  	Checks             *ChecksService
   197  	CodeScanning       *CodeScanningService
   198  	CodesOfConduct     *CodesOfConductService
   199  	Codespaces         *CodespacesService
   200  	Copilot            *CopilotService
   201  	Dependabot         *DependabotService
   202  	DependencyGraph    *DependencyGraphService
   203  	Emojis             *EmojisService
   204  	Enterprise         *EnterpriseService
   205  	Gists              *GistsService
   206  	Git                *GitService
   207  	Gitignores         *GitignoresService
   208  	Interactions       *InteractionsService
   209  	IssueImport        *IssueImportService
   210  	Issues             *IssuesService
   211  	Licenses           *LicensesService
   212  	Markdown           *MarkdownService
   213  	Marketplace        *MarketplaceService
   214  	Meta               *MetaService
   215  	Migrations         *MigrationService
   216  	Organizations      *OrganizationsService
   217  	PullRequests       *PullRequestsService
   218  	RateLimit          *RateLimitService
   219  	Reactions          *ReactionsService
   220  	Repositories       *RepositoriesService
   221  	SCIM               *SCIMService
   222  	Search             *SearchService
   223  	SecretScanning     *SecretScanningService
   224  	SecurityAdvisories *SecurityAdvisoriesService
   225  	Teams              *TeamsService
   226  	Users              *UsersService
   227  }
   228  
   229  type service struct {
   230  	client *Client
   231  }
   232  
   233  // Client returns the http.Client used by this GitHub client.
   234  // This should only be used for requests to the GitHub API because
   235  // request headers will contain an authorization token.
   236  func (c *Client) Client() *http.Client {
   237  	c.clientMu.Lock()
   238  	defer c.clientMu.Unlock()
   239  	clientCopy := *c.client
   240  	return &clientCopy
   241  }
   242  
   243  // ListOptions specifies the optional parameters to various List methods that
   244  // support offset pagination.
   245  type ListOptions struct {
   246  	// For paginated result sets, page of results to retrieve.
   247  	Page int `url:"page,omitempty"`
   248  
   249  	// For paginated result sets, the number of results to include per page.
   250  	PerPage int `url:"per_page,omitempty"`
   251  }
   252  
   253  // ListCursorOptions specifies the optional parameters to various List methods that
   254  // support cursor pagination.
   255  type ListCursorOptions struct {
   256  	// For paginated result sets, page of results to retrieve.
   257  	Page string `url:"page,omitempty"`
   258  
   259  	// For paginated result sets, the number of results to include per page.
   260  	PerPage int `url:"per_page,omitempty"`
   261  
   262  	// For paginated result sets, the number of results per page (max 100), starting from the first matching result.
   263  	// This parameter must not be used in combination with last.
   264  	First int `url:"first,omitempty"`
   265  
   266  	// For paginated result sets, the number of results per page (max 100), starting from the last matching result.
   267  	// This parameter must not be used in combination with first.
   268  	Last int `url:"last,omitempty"`
   269  
   270  	// A cursor, as given in the Link header. If specified, the query only searches for events after this cursor.
   271  	After string `url:"after,omitempty"`
   272  
   273  	// A cursor, as given in the Link header. If specified, the query only searches for events before this cursor.
   274  	Before string `url:"before,omitempty"`
   275  
   276  	// A cursor, as given in the Link header. If specified, the query continues the search using this cursor.
   277  	Cursor string `url:"cursor,omitempty"`
   278  }
   279  
   280  // UploadOptions specifies the parameters to methods that support uploads.
   281  type UploadOptions struct {
   282  	Name      string `url:"name,omitempty"`
   283  	Label     string `url:"label,omitempty"`
   284  	MediaType string `url:"-"`
   285  }
   286  
   287  // RawType represents type of raw format of a request instead of JSON.
   288  type RawType uint8
   289  
   290  const (
   291  	// Diff format.
   292  	Diff RawType = 1 + iota
   293  	// Patch format.
   294  	Patch
   295  )
   296  
   297  // RawOptions specifies parameters when user wants to get raw format of
   298  // a response instead of JSON.
   299  type RawOptions struct {
   300  	Type RawType
   301  }
   302  
   303  // addOptions adds the parameters in opts as URL query parameters to s. opts
   304  // must be a struct whose fields may contain "url" tags.
   305  func addOptions(s string, opts interface{}) (string, error) {
   306  	v := reflect.ValueOf(opts)
   307  	if v.Kind() == reflect.Ptr && v.IsNil() {
   308  		return s, nil
   309  	}
   310  
   311  	u, err := url.Parse(s)
   312  	if err != nil {
   313  		return s, err
   314  	}
   315  
   316  	qs, err := query.Values(opts)
   317  	if err != nil {
   318  		return s, err
   319  	}
   320  
   321  	u.RawQuery = qs.Encode()
   322  	return u.String(), nil
   323  }
   324  
   325  // NewClient returns a new GitHub API client. If a nil httpClient is
   326  // provided, a new http.Client will be used. To use API methods which require
   327  // authentication, either use Client.WithAuthToken or provide NewClient with
   328  // an http.Client that will perform the authentication for you (such as that
   329  // provided by the golang.org/x/oauth2 library).
   330  func NewClient(httpClient *http.Client) *Client {
   331  	if httpClient == nil {
   332  		httpClient = &http.Client{}
   333  	}
   334  	httpClient2 := *httpClient
   335  	c := &Client{client: &httpClient2}
   336  	c.initialize()
   337  	return c
   338  }
   339  
   340  // WithAuthToken returns a copy of the client configured to use the provided token for the Authorization header.
   341  func (c *Client) WithAuthToken(token string) *Client {
   342  	c2 := c.copy()
   343  	defer c2.initialize()
   344  	transport := c2.client.Transport
   345  	if transport == nil {
   346  		transport = http.DefaultTransport
   347  	}
   348  	c2.client.Transport = roundTripperFunc(
   349  		func(req *http.Request) (*http.Response, error) {
   350  			req = req.Clone(req.Context())
   351  			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
   352  			return transport.RoundTrip(req)
   353  		},
   354  	)
   355  	return c2
   356  }
   357  
   358  // WithEnterpriseURLs returns a copy of the client configured to use the provided base and
   359  // upload URLs. If the base URL does not have the suffix "/api/v3/", it will be added
   360  // automatically. If the upload URL does not have the suffix "/api/uploads", it will be
   361  // added automatically.
   362  //
   363  // Note that WithEnterpriseURLs is a convenience helper only;
   364  // its behavior is equivalent to setting the BaseURL and UploadURL fields.
   365  //
   366  // Another important thing is that by default, the GitHub Enterprise URL format
   367  // should be http(s)://[hostname]/api/v3/ or you will always receive the 406 status code.
   368  // The upload URL format should be http(s)://[hostname]/api/uploads/.
   369  func (c *Client) WithEnterpriseURLs(baseURL, uploadURL string) (*Client, error) {
   370  	c2 := c.copy()
   371  	defer c2.initialize()
   372  	var err error
   373  	c2.BaseURL, err = url.Parse(baseURL)
   374  	if err != nil {
   375  		return nil, err
   376  	}
   377  
   378  	if !strings.HasSuffix(c2.BaseURL.Path, "/") {
   379  		c2.BaseURL.Path += "/"
   380  	}
   381  	if !strings.HasSuffix(c2.BaseURL.Path, "/api/v3/") &&
   382  		!strings.HasPrefix(c2.BaseURL.Host, "api.") &&
   383  		!strings.Contains(c2.BaseURL.Host, ".api.") {
   384  		c2.BaseURL.Path += "api/v3/"
   385  	}
   386  
   387  	c2.UploadURL, err = url.Parse(uploadURL)
   388  	if err != nil {
   389  		return nil, err
   390  	}
   391  
   392  	if !strings.HasSuffix(c2.UploadURL.Path, "/") {
   393  		c2.UploadURL.Path += "/"
   394  	}
   395  	if !strings.HasSuffix(c2.UploadURL.Path, "/api/uploads/") &&
   396  		!strings.HasPrefix(c2.UploadURL.Host, "api.") &&
   397  		!strings.Contains(c2.UploadURL.Host, ".api.") {
   398  		c2.UploadURL.Path += "api/uploads/"
   399  	}
   400  	return c2, nil
   401  }
   402  
   403  // initialize sets default values and initializes services.
   404  func (c *Client) initialize() {
   405  	if c.client == nil {
   406  		c.client = &http.Client{}
   407  	}
   408  	// Copy the main http client into the IgnoreRedirects one, overriding the `CheckRedirect` func
   409  	c.clientIgnoreRedirects = &http.Client{}
   410  	c.clientIgnoreRedirects.Transport = c.client.Transport
   411  	c.clientIgnoreRedirects.Timeout = c.client.Timeout
   412  	c.clientIgnoreRedirects.Jar = c.client.Jar
   413  	c.clientIgnoreRedirects.CheckRedirect = func(req *http.Request, via []*http.Request) error {
   414  		return http.ErrUseLastResponse
   415  	}
   416  	if c.BaseURL == nil {
   417  		c.BaseURL, _ = url.Parse(defaultBaseURL)
   418  	}
   419  	if c.UploadURL == nil {
   420  		c.UploadURL, _ = url.Parse(uploadBaseURL)
   421  	}
   422  	if c.UserAgent == "" {
   423  		c.UserAgent = defaultUserAgent
   424  	}
   425  	c.common.client = c
   426  	c.Actions = (*ActionsService)(&c.common)
   427  	c.Activity = (*ActivityService)(&c.common)
   428  	c.Admin = (*AdminService)(&c.common)
   429  	c.Apps = (*AppsService)(&c.common)
   430  	c.Authorizations = (*AuthorizationsService)(&c.common)
   431  	c.Billing = (*BillingService)(&c.common)
   432  	c.Checks = (*ChecksService)(&c.common)
   433  	c.CodeScanning = (*CodeScanningService)(&c.common)
   434  	c.Codespaces = (*CodespacesService)(&c.common)
   435  	c.CodesOfConduct = (*CodesOfConductService)(&c.common)
   436  	c.Copilot = (*CopilotService)(&c.common)
   437  	c.Dependabot = (*DependabotService)(&c.common)
   438  	c.DependencyGraph = (*DependencyGraphService)(&c.common)
   439  	c.Emojis = (*EmojisService)(&c.common)
   440  	c.Enterprise = (*EnterpriseService)(&c.common)
   441  	c.Gists = (*GistsService)(&c.common)
   442  	c.Git = (*GitService)(&c.common)
   443  	c.Gitignores = (*GitignoresService)(&c.common)
   444  	c.Interactions = (*InteractionsService)(&c.common)
   445  	c.IssueImport = (*IssueImportService)(&c.common)
   446  	c.Issues = (*IssuesService)(&c.common)
   447  	c.Licenses = (*LicensesService)(&c.common)
   448  	c.Markdown = (*MarkdownService)(&c.common)
   449  	c.Marketplace = &MarketplaceService{client: c}
   450  	c.Meta = (*MetaService)(&c.common)
   451  	c.Migrations = (*MigrationService)(&c.common)
   452  	c.Organizations = (*OrganizationsService)(&c.common)
   453  	c.PullRequests = (*PullRequestsService)(&c.common)
   454  	c.RateLimit = (*RateLimitService)(&c.common)
   455  	c.Reactions = (*ReactionsService)(&c.common)
   456  	c.Repositories = (*RepositoriesService)(&c.common)
   457  	c.SCIM = (*SCIMService)(&c.common)
   458  	c.Search = (*SearchService)(&c.common)
   459  	c.SecretScanning = (*SecretScanningService)(&c.common)
   460  	c.SecurityAdvisories = (*SecurityAdvisoriesService)(&c.common)
   461  	c.Teams = (*TeamsService)(&c.common)
   462  	c.Users = (*UsersService)(&c.common)
   463  }
   464  
   465  // copy returns a copy of the current client. It must be initialized before use.
   466  func (c *Client) copy() *Client {
   467  	c.clientMu.Lock()
   468  	// can't use *c here because that would copy mutexes by value.
   469  	clone := Client{
   470  		client:                          &http.Client{},
   471  		UserAgent:                       c.UserAgent,
   472  		BaseURL:                         c.BaseURL,
   473  		UploadURL:                       c.UploadURL,
   474  		RateLimitRedirectionalEndpoints: c.RateLimitRedirectionalEndpoints,
   475  		secondaryRateLimitReset:         c.secondaryRateLimitReset,
   476  	}
   477  	c.clientMu.Unlock()
   478  	if c.client != nil {
   479  		clone.client.Transport = c.client.Transport
   480  		clone.client.CheckRedirect = c.client.CheckRedirect
   481  		clone.client.Jar = c.client.Jar
   482  		clone.client.Timeout = c.client.Timeout
   483  	}
   484  	c.rateMu.Lock()
   485  	copy(clone.rateLimits[:], c.rateLimits[:])
   486  	c.rateMu.Unlock()
   487  	return &clone
   488  }
   489  
   490  // NewClientWithEnvProxy enhances NewClient with the HttpProxy env.
   491  func NewClientWithEnvProxy() *Client {
   492  	return NewClient(&http.Client{Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}})
   493  }
   494  
   495  // NewTokenClient returns a new GitHub API client authenticated with the provided token.
   496  // Deprecated: Use NewClient(nil).WithAuthToken(token) instead.
   497  func NewTokenClient(_ context.Context, token string) *Client {
   498  	// This always returns a nil error.
   499  	return NewClient(nil).WithAuthToken(token)
   500  }
   501  
   502  // NewEnterpriseClient returns a new GitHub API client with provided
   503  // base URL and upload URL (often is your GitHub Enterprise hostname).
   504  //
   505  // Deprecated: Use NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) instead.
   506  func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) {
   507  	return NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL)
   508  }
   509  
   510  // RequestOption represents an option that can modify an http.Request.
   511  type RequestOption func(req *http.Request)
   512  
   513  // WithVersion overrides the GitHub v3 API version for this individual request.
   514  // For more information, see:
   515  // https://github.blog/2022-11-28-to-infinity-and-beyond-enabling-the-future-of-githubs-rest-api-with-api-versioning/
   516  func WithVersion(version string) RequestOption {
   517  	return func(req *http.Request) {
   518  		req.Header.Set(headerAPIVersion, version)
   519  	}
   520  }
   521  
   522  // NewRequest creates an API request. A relative URL can be provided in urlStr,
   523  // in which case it is resolved relative to the BaseURL of the Client.
   524  // Relative URLs should always be specified without a preceding slash. If
   525  // specified, the value pointed to by body is JSON encoded and included as the
   526  // request body.
   527  func (c *Client) NewRequest(method, urlStr string, body interface{}, opts ...RequestOption) (*http.Request, error) {
   528  	if !strings.HasSuffix(c.BaseURL.Path, "/") {
   529  		return nil, fmt.Errorf("baseURL must have a trailing slash, but %q does not", c.BaseURL)
   530  	}
   531  
   532  	u, err := c.BaseURL.Parse(urlStr)
   533  	if err != nil {
   534  		return nil, err
   535  	}
   536  
   537  	var buf io.ReadWriter
   538  	if body != nil {
   539  		buf = &bytes.Buffer{}
   540  		enc := json.NewEncoder(buf)
   541  		enc.SetEscapeHTML(false)
   542  		err := enc.Encode(body)
   543  		if err != nil {
   544  			return nil, err
   545  		}
   546  	}
   547  
   548  	req, err := http.NewRequest(method, u.String(), buf)
   549  	if err != nil {
   550  		return nil, err
   551  	}
   552  
   553  	if body != nil {
   554  		req.Header.Set("Content-Type", "application/json")
   555  	}
   556  	req.Header.Set("Accept", mediaTypeV3)
   557  	if c.UserAgent != "" {
   558  		req.Header.Set("User-Agent", c.UserAgent)
   559  	}
   560  	req.Header.Set(headerAPIVersion, defaultAPIVersion)
   561  
   562  	for _, opt := range opts {
   563  		opt(req)
   564  	}
   565  
   566  	return req, nil
   567  }
   568  
   569  // NewFormRequest creates an API request. A relative URL can be provided in urlStr,
   570  // in which case it is resolved relative to the BaseURL of the Client.
   571  // Relative URLs should always be specified without a preceding slash.
   572  // Body is sent with Content-Type: application/x-www-form-urlencoded.
   573  func (c *Client) NewFormRequest(urlStr string, body io.Reader, opts ...RequestOption) (*http.Request, error) {
   574  	if !strings.HasSuffix(c.BaseURL.Path, "/") {
   575  		return nil, fmt.Errorf("baseURL must have a trailing slash, but %q does not", c.BaseURL)
   576  	}
   577  
   578  	u, err := c.BaseURL.Parse(urlStr)
   579  	if err != nil {
   580  		return nil, err
   581  	}
   582  
   583  	req, err := http.NewRequest(http.MethodPost, u.String(), body)
   584  	if err != nil {
   585  		return nil, err
   586  	}
   587  
   588  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   589  	req.Header.Set("Accept", mediaTypeV3)
   590  	if c.UserAgent != "" {
   591  		req.Header.Set("User-Agent", c.UserAgent)
   592  	}
   593  	req.Header.Set(headerAPIVersion, defaultAPIVersion)
   594  
   595  	for _, opt := range opts {
   596  		opt(req)
   597  	}
   598  
   599  	return req, nil
   600  }
   601  
   602  // NewUploadRequest creates an upload request. A relative URL can be provided in
   603  // urlStr, in which case it is resolved relative to the UploadURL of the Client.
   604  // Relative URLs should always be specified without a preceding slash.
   605  func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string, opts ...RequestOption) (*http.Request, error) {
   606  	if !strings.HasSuffix(c.UploadURL.Path, "/") {
   607  		return nil, fmt.Errorf("uploadURL must have a trailing slash, but %q does not", c.UploadURL)
   608  	}
   609  	u, err := c.UploadURL.Parse(urlStr)
   610  	if err != nil {
   611  		return nil, err
   612  	}
   613  
   614  	req, err := http.NewRequest("POST", u.String(), reader)
   615  	if err != nil {
   616  		return nil, err
   617  	}
   618  
   619  	req.ContentLength = size
   620  
   621  	if mediaType == "" {
   622  		mediaType = defaultMediaType
   623  	}
   624  	req.Header.Set("Content-Type", mediaType)
   625  	req.Header.Set("Accept", mediaTypeV3)
   626  	req.Header.Set("User-Agent", c.UserAgent)
   627  	req.Header.Set(headerAPIVersion, defaultAPIVersion)
   628  
   629  	for _, opt := range opts {
   630  		opt(req)
   631  	}
   632  
   633  	return req, nil
   634  }
   635  
   636  // Response is a GitHub API response. This wraps the standard http.Response
   637  // returned from GitHub and provides convenient access to things like
   638  // pagination links.
   639  type Response struct {
   640  	*http.Response
   641  
   642  	// These fields provide the page values for paginating through a set of
   643  	// results. Any or all of these may be set to the zero value for
   644  	// responses that are not part of a paginated set, or for which there
   645  	// are no additional pages.
   646  	//
   647  	// These fields support what is called "offset pagination" and should
   648  	// be used with the ListOptions struct.
   649  	NextPage  int
   650  	PrevPage  int
   651  	FirstPage int
   652  	LastPage  int
   653  
   654  	// Additionally, some APIs support "cursor pagination" instead of offset.
   655  	// This means that a token points directly to the next record which
   656  	// can lead to O(1) performance compared to O(n) performance provided
   657  	// by offset pagination.
   658  	//
   659  	// For APIs that support cursor pagination (such as
   660  	// TeamsService.ListIDPGroupsInOrganization), the following field
   661  	// will be populated to point to the next page.
   662  	//
   663  	// To use this token, set ListCursorOptions.Page to this value before
   664  	// calling the endpoint again.
   665  	NextPageToken string
   666  
   667  	// For APIs that support cursor pagination, such as RepositoriesService.ListHookDeliveries,
   668  	// the following field will be populated to point to the next page.
   669  	// Set ListCursorOptions.Cursor to this value when calling the endpoint again.
   670  	Cursor string
   671  
   672  	// For APIs that support before/after pagination, such as OrganizationsService.AuditLog.
   673  	Before string
   674  	After  string
   675  
   676  	// Explicitly specify the Rate type so Rate's String() receiver doesn't
   677  	// propagate to Response.
   678  	Rate Rate
   679  
   680  	// token's expiration date. Timestamp is 0001-01-01 when token doesn't expire.
   681  	// So it is valid for TokenExpiration.Equal(Timestamp{}) or TokenExpiration.Time.After(time.Now())
   682  	TokenExpiration Timestamp
   683  }
   684  
   685  // newResponse creates a new Response for the provided http.Response.
   686  // r must not be nil.
   687  func newResponse(r *http.Response) *Response {
   688  	response := &Response{Response: r}
   689  	response.populatePageValues()
   690  	response.Rate = parseRate(r)
   691  	response.TokenExpiration = parseTokenExpiration(r)
   692  	return response
   693  }
   694  
   695  // populatePageValues parses the HTTP Link response headers and populates the
   696  // various pagination link values in the Response.
   697  func (r *Response) populatePageValues() {
   698  	if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 {
   699  		for _, link := range strings.Split(links[0], ",") {
   700  			segments := strings.Split(strings.TrimSpace(link), ";")
   701  
   702  			// link must at least have href and rel
   703  			if len(segments) < 2 {
   704  				continue
   705  			}
   706  
   707  			// ensure href is properly formatted
   708  			if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") {
   709  				continue
   710  			}
   711  
   712  			// try to pull out page parameter
   713  			url, err := url.Parse(segments[0][1 : len(segments[0])-1])
   714  			if err != nil {
   715  				continue
   716  			}
   717  
   718  			q := url.Query()
   719  
   720  			if cursor := q.Get("cursor"); cursor != "" {
   721  				for _, segment := range segments[1:] {
   722  					switch strings.TrimSpace(segment) {
   723  					case `rel="next"`:
   724  						r.Cursor = cursor
   725  					}
   726  				}
   727  
   728  				continue
   729  			}
   730  
   731  			page := q.Get("page")
   732  			since := q.Get("since")
   733  			before := q.Get("before")
   734  			after := q.Get("after")
   735  
   736  			if page == "" && before == "" && after == "" && since == "" {
   737  				continue
   738  			}
   739  
   740  			if since != "" && page == "" {
   741  				page = since
   742  			}
   743  
   744  			for _, segment := range segments[1:] {
   745  				switch strings.TrimSpace(segment) {
   746  				case `rel="next"`:
   747  					if r.NextPage, err = strconv.Atoi(page); err != nil {
   748  						r.NextPageToken = page
   749  					}
   750  					r.After = after
   751  				case `rel="prev"`:
   752  					r.PrevPage, _ = strconv.Atoi(page)
   753  					r.Before = before
   754  				case `rel="first"`:
   755  					r.FirstPage, _ = strconv.Atoi(page)
   756  				case `rel="last"`:
   757  					r.LastPage, _ = strconv.Atoi(page)
   758  				}
   759  			}
   760  		}
   761  	}
   762  }
   763  
   764  // parseRate parses the rate related headers.
   765  func parseRate(r *http.Response) Rate {
   766  	var rate Rate
   767  	if limit := r.Header.Get(headerRateLimit); limit != "" {
   768  		rate.Limit, _ = strconv.Atoi(limit)
   769  	}
   770  	if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
   771  		rate.Remaining, _ = strconv.Atoi(remaining)
   772  	}
   773  	if used := r.Header.Get(headerRateUsed); used != "" {
   774  		rate.Used, _ = strconv.Atoi(used)
   775  	}
   776  	if reset := r.Header.Get(headerRateReset); reset != "" {
   777  		if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
   778  			rate.Reset = Timestamp{time.Unix(v, 0)}
   779  		}
   780  	}
   781  	if resource := r.Header.Get(headerRateResource); resource != "" {
   782  		rate.Resource = resource
   783  	}
   784  	return rate
   785  }
   786  
   787  // parseSecondaryRate parses the secondary rate related headers,
   788  // and returns the time to retry after.
   789  func parseSecondaryRate(r *http.Response) *time.Duration {
   790  	// According to GitHub support, the "Retry-After" header value will be
   791  	// an integer which represents the number of seconds that one should
   792  	// wait before resuming making requests.
   793  	if v := r.Header.Get(headerRetryAfter); v != "" {
   794  		retryAfterSeconds, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop.
   795  		retryAfter := time.Duration(retryAfterSeconds) * time.Second
   796  		return &retryAfter
   797  	}
   798  
   799  	// According to GitHub support, endpoints might return x-ratelimit-reset instead,
   800  	// as an integer which represents the number of seconds since epoch UTC,
   801  	// representing the time to resume making requests.
   802  	if v := r.Header.Get(headerRateReset); v != "" {
   803  		secondsSinceEpoch, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop.
   804  		retryAfter := time.Until(time.Unix(secondsSinceEpoch, 0))
   805  		return &retryAfter
   806  	}
   807  
   808  	return nil
   809  }
   810  
   811  // parseTokenExpiration parses the TokenExpiration related headers.
   812  // Returns 0001-01-01 if the header is not defined or could not be parsed.
   813  func parseTokenExpiration(r *http.Response) Timestamp {
   814  	if v := r.Header.Get(headerTokenExpiration); v != "" {
   815  		if t, err := time.Parse("2006-01-02 15:04:05 MST", v); err == nil {
   816  			return Timestamp{t.Local()}
   817  		}
   818  		// Some tokens include the timezone offset instead of the timezone.
   819  		// https://github.com/google/go-github/issues/2649
   820  		if t, err := time.Parse("2006-01-02 15:04:05 -0700", v); err == nil {
   821  			return Timestamp{t.Local()}
   822  		}
   823  	}
   824  	return Timestamp{} // 0001-01-01 00:00:00
   825  }
   826  
   827  type requestContext uint8
   828  
   829  const (
   830  	// BypassRateLimitCheck prevents a pre-emptive check for exceeded primary rate limits
   831  	// Specify this by providing a context with this key, e.g.
   832  	//   context.WithValue(context.Background(), github.BypassRateLimitCheck, true)
   833  	BypassRateLimitCheck requestContext = iota
   834  
   835  	SleepUntilPrimaryRateLimitResetWhenRateLimited
   836  )
   837  
   838  // bareDo sends an API request using `caller` http.Client passed in the parameters
   839  // and lets you handle the api response. If an error or API Error occurs, the error
   840  // will contain more information. Otherwise you are supposed to read and close the
   841  // response's Body. If rate limit is exceeded and reset time is in the future,
   842  // bareDo returns *RateLimitError immediately without making a network API call.
   843  //
   844  // The provided ctx must be non-nil, if it is nil an error is returned. If it is
   845  // canceled or times out, ctx.Err() will be returned.
   846  func (c *Client) bareDo(ctx context.Context, caller *http.Client, req *http.Request) (*Response, error) {
   847  	if ctx == nil {
   848  		return nil, errNonNilContext
   849  	}
   850  
   851  	req = withContext(ctx, req)
   852  
   853  	rateLimitCategory := GetRateLimitCategory(req.Method, req.URL.Path)
   854  
   855  	if bypass := ctx.Value(BypassRateLimitCheck); bypass == nil {
   856  		// If we've hit rate limit, don't make further requests before Reset time.
   857  		if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil {
   858  			return &Response{
   859  				Response: err.Response,
   860  				Rate:     err.Rate,
   861  			}, err
   862  		}
   863  		// If we've hit a secondary rate limit, don't make further requests before Retry After.
   864  		if err := c.checkSecondaryRateLimitBeforeDo(req); err != nil {
   865  			return &Response{
   866  				Response: err.Response,
   867  			}, err
   868  		}
   869  	}
   870  
   871  	resp, err := caller.Do(req)
   872  	var response *Response
   873  	if resp != nil {
   874  		response = newResponse(resp)
   875  	}
   876  
   877  	if err != nil {
   878  		// If we got an error, and the context has been canceled,
   879  		// the context's error is probably more useful.
   880  		select {
   881  		case <-ctx.Done():
   882  			return response, ctx.Err()
   883  		default:
   884  		}
   885  
   886  		// If the error type is *url.Error, sanitize its URL before returning.
   887  		if e, ok := err.(*url.Error); ok {
   888  			if url, err := url.Parse(e.URL); err == nil {
   889  				e.URL = sanitizeURL(url).String()
   890  				return response, e
   891  			}
   892  		}
   893  
   894  		return response, err
   895  	}
   896  
   897  	// Don't update the rate limits if this was a cached response.
   898  	// X-From-Cache is set by https://github.com/gregjones/httpcache
   899  	if response.Header.Get("X-From-Cache") == "" {
   900  		c.rateMu.Lock()
   901  		c.rateLimits[rateLimitCategory] = response.Rate
   902  		c.rateMu.Unlock()
   903  	}
   904  
   905  	err = CheckResponse(resp)
   906  	if err != nil {
   907  		defer resp.Body.Close()
   908  		// Special case for AcceptedErrors. If an AcceptedError
   909  		// has been encountered, the response's payload will be
   910  		// added to the AcceptedError and returned.
   911  		//
   912  		// Issue #1022
   913  		aerr, ok := err.(*AcceptedError)
   914  		if ok {
   915  			b, readErr := io.ReadAll(resp.Body)
   916  			if readErr != nil {
   917  				return response, readErr
   918  			}
   919  
   920  			aerr.Raw = b
   921  			err = aerr
   922  		}
   923  
   924  		rateLimitError, ok := err.(*RateLimitError)
   925  		if ok && req.Context().Value(SleepUntilPrimaryRateLimitResetWhenRateLimited) != nil {
   926  			if err := sleepUntilResetWithBuffer(req.Context(), rateLimitError.Rate.Reset.Time); err != nil {
   927  				return response, err
   928  			}
   929  			// retry the request once when the rate limit has reset
   930  			return c.bareDo(context.WithValue(req.Context(), SleepUntilPrimaryRateLimitResetWhenRateLimited, nil), caller, req)
   931  		}
   932  
   933  		// Update the secondary rate limit if we hit it.
   934  		rerr, ok := err.(*AbuseRateLimitError)
   935  		if ok && rerr.RetryAfter != nil {
   936  			// if a max duration is specified, make sure that we are waiting at most this duration
   937  			if c.MaxSecondaryRateLimitRetryAfterDuration > 0 && rerr.GetRetryAfter() > c.MaxSecondaryRateLimitRetryAfterDuration {
   938  				rerr.RetryAfter = &c.MaxSecondaryRateLimitRetryAfterDuration
   939  			}
   940  			c.rateMu.Lock()
   941  			c.secondaryRateLimitReset = time.Now().Add(*rerr.RetryAfter)
   942  			c.rateMu.Unlock()
   943  		}
   944  	}
   945  	return response, err
   946  }
   947  
   948  // BareDo sends an API request and lets you handle the api response. If an error
   949  // or API Error occurs, the error will contain more information. Otherwise you
   950  // are supposed to read and close the response's Body. If rate limit is exceeded
   951  // and reset time is in the future, BareDo returns *RateLimitError immediately
   952  // without making a network API call.
   953  //
   954  // The provided ctx must be non-nil, if it is nil an error is returned. If it is
   955  // canceled or times out, ctx.Err() will be returned.
   956  func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, error) {
   957  	return c.bareDo(ctx, c.client, req)
   958  }
   959  
   960  // bareDoIgnoreRedirects has the exact same behavior as BareDo but stops at the first
   961  // redirection code returned by the API. If a redirection is returned by the api, bareDoIgnoreRedirects
   962  // returns a *RedirectionError.
   963  //
   964  // The provided ctx must be non-nil, if it is nil an error is returned. If it is
   965  // canceled or times out, ctx.Err() will be returned.
   966  func (c *Client) bareDoIgnoreRedirects(ctx context.Context, req *http.Request) (*Response, error) {
   967  	return c.bareDo(ctx, c.clientIgnoreRedirects, req)
   968  }
   969  
   970  var errInvalidLocation = errors.New("invalid or empty Location header in redirection response")
   971  
   972  // bareDoUntilFound has the exact same behavior as BareDo but only follows 301s, up to maxRedirects times. If it receives
   973  // a 302, it will parse the Location header into a *url.URL and return that.
   974  // This is useful for endpoints that return a 302 in successful cases but still might return 301s for
   975  // permanent redirections.
   976  //
   977  // The provided ctx must be non-nil, if it is nil an error is returned. If it is
   978  // canceled or times out, ctx.Err() will be returned.
   979  func (c *Client) bareDoUntilFound(ctx context.Context, req *http.Request, maxRedirects int) (*url.URL, *Response, error) {
   980  	response, err := c.bareDoIgnoreRedirects(ctx, req)
   981  	if err != nil {
   982  		rerr, ok := err.(*RedirectionError)
   983  		if ok {
   984  			// If we receive a 302, transform potential relative locations into absolute and return it.
   985  			if rerr.StatusCode == http.StatusFound {
   986  				if rerr.Location == nil {
   987  					return nil, nil, errInvalidLocation
   988  				}
   989  				newURL := c.BaseURL.ResolveReference(rerr.Location)
   990  				return newURL, response, nil
   991  			}
   992  			// If permanent redirect response is returned, follow it
   993  			if maxRedirects > 0 && rerr.StatusCode == http.StatusMovedPermanently {
   994  				if rerr.Location == nil {
   995  					return nil, nil, errInvalidLocation
   996  				}
   997  				newURL := c.BaseURL.ResolveReference(rerr.Location)
   998  				newRequest := req.Clone(ctx)
   999  				newRequest.URL = newURL
  1000  				return c.bareDoUntilFound(ctx, newRequest, maxRedirects-1)
  1001  			}
  1002  			// If we reached the maximum amount of redirections, return an error
  1003  			if maxRedirects <= 0 && rerr.StatusCode == http.StatusMovedPermanently {
  1004  				return nil, response, fmt.Errorf("reached the maximum amount of redirections: %w", err)
  1005  			}
  1006  			return nil, response, fmt.Errorf("unexpected redirection response: %w", err)
  1007  		}
  1008  	}
  1009  
  1010  	// If we don't receive a redirection, forward the response and potential error
  1011  	return nil, response, err
  1012  }
  1013  
  1014  // Do sends an API request and returns the API response. The API response is
  1015  // JSON decoded and stored in the value pointed to by v, or returned as an
  1016  // error if an API error has occurred. If v implements the io.Writer interface,
  1017  // the raw response body will be written to v, without attempting to first
  1018  // decode it. If v is nil, and no error happens, the response is returned as is.
  1019  // If rate limit is exceeded and reset time is in the future, Do returns
  1020  // *RateLimitError immediately without making a network API call.
  1021  //
  1022  // The provided ctx must be non-nil, if it is nil an error is returned. If it
  1023  // is canceled or times out, ctx.Err() will be returned.
  1024  func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
  1025  	resp, err := c.BareDo(ctx, req)
  1026  	if err != nil {
  1027  		return resp, err
  1028  	}
  1029  	defer resp.Body.Close()
  1030  
  1031  	switch v := v.(type) {
  1032  	case nil:
  1033  	case io.Writer:
  1034  		_, err = io.Copy(v, resp.Body)
  1035  	default:
  1036  		decErr := json.NewDecoder(resp.Body).Decode(v)
  1037  		if decErr == io.EOF {
  1038  			decErr = nil // ignore EOF errors caused by empty response body
  1039  		}
  1040  		if decErr != nil {
  1041  			err = decErr
  1042  		}
  1043  	}
  1044  	return resp, err
  1045  }
  1046  
  1047  // checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from
  1048  // current client state in order to quickly check if *RateLimitError can be immediately returned
  1049  // from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily.
  1050  // Otherwise it returns nil, and Client.Do should proceed normally.
  1051  func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory RateLimitCategory) *RateLimitError {
  1052  	c.rateMu.Lock()
  1053  	rate := c.rateLimits[rateLimitCategory]
  1054  	c.rateMu.Unlock()
  1055  	if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) {
  1056  		// Create a fake response.
  1057  		resp := &http.Response{
  1058  			Status:     http.StatusText(http.StatusForbidden),
  1059  			StatusCode: http.StatusForbidden,
  1060  			Request:    req,
  1061  			Header:     make(http.Header),
  1062  			Body:       io.NopCloser(strings.NewReader("")),
  1063  		}
  1064  
  1065  		if req.Context().Value(SleepUntilPrimaryRateLimitResetWhenRateLimited) != nil {
  1066  			if err := sleepUntilResetWithBuffer(req.Context(), rate.Reset.Time); err == nil {
  1067  				return nil
  1068  			}
  1069  			return &RateLimitError{
  1070  				Rate:     rate,
  1071  				Response: resp,
  1072  				Message:  fmt.Sprintf("Context cancelled while waiting for rate limit to reset until %v, not making remote request.", rate.Reset.Time),
  1073  			}
  1074  		}
  1075  
  1076  		return &RateLimitError{
  1077  			Rate:     rate,
  1078  			Response: resp,
  1079  			Message:  fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time),
  1080  		}
  1081  	}
  1082  
  1083  	return nil
  1084  }
  1085  
  1086  // checkSecondaryRateLimitBeforeDo does not make any network calls, but uses existing knowledge from
  1087  // current client state in order to quickly check if *AbuseRateLimitError can be immediately returned
  1088  // from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily.
  1089  // Otherwise it returns nil, and Client.Do should proceed normally.
  1090  func (c *Client) checkSecondaryRateLimitBeforeDo(req *http.Request) *AbuseRateLimitError {
  1091  	c.rateMu.Lock()
  1092  	secondary := c.secondaryRateLimitReset
  1093  	c.rateMu.Unlock()
  1094  	if !secondary.IsZero() && time.Now().Before(secondary) {
  1095  		// Create a fake response.
  1096  		resp := &http.Response{
  1097  			Status:     http.StatusText(http.StatusForbidden),
  1098  			StatusCode: http.StatusForbidden,
  1099  			Request:    req,
  1100  			Header:     make(http.Header),
  1101  			Body:       io.NopCloser(strings.NewReader("")),
  1102  		}
  1103  
  1104  		retryAfter := time.Until(secondary)
  1105  		return &AbuseRateLimitError{
  1106  			Response:   resp,
  1107  			Message:    fmt.Sprintf("API secondary rate limit exceeded until %v, not making remote request.", secondary),
  1108  			RetryAfter: &retryAfter,
  1109  		}
  1110  	}
  1111  
  1112  	return nil
  1113  }
  1114  
  1115  // compareHTTPResponse returns whether two http.Response objects are equal or not.
  1116  // Currently, only StatusCode is checked. This function is used when implementing the
  1117  // Is(error) bool interface for the custom error types in this package.
  1118  func compareHTTPResponse(r1, r2 *http.Response) bool {
  1119  	if r1 == nil && r2 == nil {
  1120  		return true
  1121  	}
  1122  
  1123  	if r1 != nil && r2 != nil {
  1124  		return r1.StatusCode == r2.StatusCode
  1125  	}
  1126  	return false
  1127  }
  1128  
  1129  /*
  1130  An ErrorResponse reports one or more errors caused by an API request.
  1131  
  1132  GitHub API docs: https://docs.github.com/rest/#client-errors
  1133  */
  1134  type ErrorResponse struct {
  1135  	Response *http.Response `json:"-"`       // HTTP response that caused this error
  1136  	Message  string         `json:"message"` // error message
  1137  	//nolint:sliceofpointers
  1138  	Errors []Error `json:"errors"` // more detail on individual errors
  1139  	// Block is only populated on certain types of errors such as code 451.
  1140  	Block *ErrorBlock `json:"block,omitempty"`
  1141  	// Most errors will also include a documentation_url field pointing
  1142  	// to some content that might help you resolve the error, see
  1143  	// https://docs.github.com/rest/#client-errors
  1144  	DocumentationURL string `json:"documentation_url,omitempty"`
  1145  }
  1146  
  1147  // ErrorBlock contains a further explanation for the reason of an error.
  1148  // See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/
  1149  // for more information.
  1150  type ErrorBlock struct {
  1151  	Reason    string     `json:"reason,omitempty"`
  1152  	CreatedAt *Timestamp `json:"created_at,omitempty"`
  1153  }
  1154  
  1155  func (r *ErrorResponse) Error() string {
  1156  	if r.Response != nil && r.Response.Request != nil {
  1157  		return fmt.Sprintf("%v %v: %d %v %+v",
  1158  			r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
  1159  			r.Response.StatusCode, r.Message, r.Errors)
  1160  	}
  1161  
  1162  	if r.Response != nil {
  1163  		return fmt.Sprintf("%d %v %+v", r.Response.StatusCode, r.Message, r.Errors)
  1164  	}
  1165  
  1166  	return fmt.Sprintf("%v %+v", r.Message, r.Errors)
  1167  }
  1168  
  1169  // Is returns whether the provided error equals this error.
  1170  func (r *ErrorResponse) Is(target error) bool {
  1171  	v, ok := target.(*ErrorResponse)
  1172  	if !ok {
  1173  		return false
  1174  	}
  1175  
  1176  	if r.Message != v.Message || (r.DocumentationURL != v.DocumentationURL) ||
  1177  		!compareHTTPResponse(r.Response, v.Response) {
  1178  		return false
  1179  	}
  1180  
  1181  	// Compare Errors.
  1182  	if len(r.Errors) != len(v.Errors) {
  1183  		return false
  1184  	}
  1185  	for idx := range r.Errors {
  1186  		if r.Errors[idx] != v.Errors[idx] {
  1187  			return false
  1188  		}
  1189  	}
  1190  
  1191  	// Compare Block.
  1192  	if (r.Block != nil && v.Block == nil) || (r.Block == nil && v.Block != nil) {
  1193  		return false
  1194  	}
  1195  	if r.Block != nil && v.Block != nil {
  1196  		if r.Block.Reason != v.Block.Reason {
  1197  			return false
  1198  		}
  1199  		if (r.Block.CreatedAt != nil && v.Block.CreatedAt == nil) || (r.Block.CreatedAt ==
  1200  			nil && v.Block.CreatedAt != nil) {
  1201  			return false
  1202  		}
  1203  		if r.Block.CreatedAt != nil && v.Block.CreatedAt != nil {
  1204  			if *(r.Block.CreatedAt) != *(v.Block.CreatedAt) {
  1205  				return false
  1206  			}
  1207  		}
  1208  	}
  1209  
  1210  	return true
  1211  }
  1212  
  1213  // TwoFactorAuthError occurs when using HTTP Basic Authentication for a user
  1214  // that has two-factor authentication enabled. The request can be reattempted
  1215  // by providing a one-time password in the request.
  1216  type TwoFactorAuthError ErrorResponse
  1217  
  1218  func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() }
  1219  
  1220  // RateLimitError occurs when GitHub returns 403 Forbidden response with a rate limit
  1221  // remaining value of 0.
  1222  type RateLimitError struct {
  1223  	Rate     Rate           // Rate specifies last known rate limit for the client
  1224  	Response *http.Response // HTTP response that caused this error
  1225  	Message  string         `json:"message"` // error message
  1226  }
  1227  
  1228  func (r *RateLimitError) Error() string {
  1229  	return fmt.Sprintf("%v %v: %d %v %v",
  1230  		r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
  1231  		r.Response.StatusCode, r.Message, formatRateReset(time.Until(r.Rate.Reset.Time)))
  1232  }
  1233  
  1234  // Is returns whether the provided error equals this error.
  1235  func (r *RateLimitError) Is(target error) bool {
  1236  	v, ok := target.(*RateLimitError)
  1237  	if !ok {
  1238  		return false
  1239  	}
  1240  
  1241  	return r.Rate == v.Rate &&
  1242  		r.Message == v.Message &&
  1243  		compareHTTPResponse(r.Response, v.Response)
  1244  }
  1245  
  1246  // AcceptedError occurs when GitHub returns 202 Accepted response with an
  1247  // empty body, which means a job was scheduled on the GitHub side to process
  1248  // the information needed and cache it.
  1249  // Technically, 202 Accepted is not a real error, it's just used to
  1250  // indicate that results are not ready yet, but should be available soon.
  1251  // The request can be repeated after some time.
  1252  type AcceptedError struct {
  1253  	// Raw contains the response body.
  1254  	Raw []byte
  1255  }
  1256  
  1257  func (*AcceptedError) Error() string {
  1258  	return "job scheduled on GitHub side; try again later"
  1259  }
  1260  
  1261  // Is returns whether the provided error equals this error.
  1262  func (ae *AcceptedError) Is(target error) bool {
  1263  	v, ok := target.(*AcceptedError)
  1264  	if !ok {
  1265  		return false
  1266  	}
  1267  	return bytes.Equal(ae.Raw, v.Raw)
  1268  }
  1269  
  1270  // AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the
  1271  // "documentation_url" field value equal to "https://docs.github.com/rest/overview/rate-limits-for-the-rest-api#about-secondary-rate-limits".
  1272  type AbuseRateLimitError struct {
  1273  	Response *http.Response // HTTP response that caused this error
  1274  	Message  string         `json:"message"` // error message
  1275  
  1276  	// RetryAfter is provided with some abuse rate limit errors. If present,
  1277  	// it is the amount of time that the client should wait before retrying.
  1278  	// Otherwise, the client should try again later (after an unspecified amount of time).
  1279  	RetryAfter *time.Duration
  1280  }
  1281  
  1282  func (r *AbuseRateLimitError) Error() string {
  1283  	return fmt.Sprintf("%v %v: %d %v",
  1284  		r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
  1285  		r.Response.StatusCode, r.Message)
  1286  }
  1287  
  1288  // Is returns whether the provided error equals this error.
  1289  func (r *AbuseRateLimitError) Is(target error) bool {
  1290  	v, ok := target.(*AbuseRateLimitError)
  1291  	if !ok {
  1292  		return false
  1293  	}
  1294  
  1295  	return r.Message == v.Message &&
  1296  		r.RetryAfter == v.RetryAfter &&
  1297  		compareHTTPResponse(r.Response, v.Response)
  1298  }
  1299  
  1300  // RedirectionError represents a response that returned a redirect status code:
  1301  //
  1302  //	301 (Moved Permanently)
  1303  //	302 (Found)
  1304  //	303 (See Other)
  1305  //	307 (Temporary Redirect)
  1306  //	308 (Permanent Redirect)
  1307  //
  1308  // If there was a valid Location header included, it will be parsed to a URL. You should use
  1309  // `BaseURL.ResolveReference()` to enrich it with the correct hostname where needed.
  1310  type RedirectionError struct {
  1311  	Response   *http.Response // HTTP response that caused this error
  1312  	StatusCode int
  1313  	Location   *url.URL // location header of the redirection if present
  1314  }
  1315  
  1316  func (r *RedirectionError) Error() string {
  1317  	return fmt.Sprintf("%v %v: %d location %v",
  1318  		r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
  1319  		r.StatusCode, sanitizeURL(r.Location))
  1320  }
  1321  
  1322  // Is returns whether the provided error equals this error.
  1323  func (r *RedirectionError) Is(target error) bool {
  1324  	v, ok := target.(*RedirectionError)
  1325  	if !ok {
  1326  		return false
  1327  	}
  1328  
  1329  	return r.StatusCode == v.StatusCode &&
  1330  		(r.Location == v.Location || // either both locations are nil or exactly the same pointer
  1331  			r.Location != nil && v.Location != nil && r.Location.String() == v.Location.String()) // or they are both not nil and marshaled identically
  1332  }
  1333  
  1334  // sanitizeURL redacts the client_secret parameter from the URL which may be
  1335  // exposed to the user.
  1336  func sanitizeURL(uri *url.URL) *url.URL {
  1337  	if uri == nil {
  1338  		return nil
  1339  	}
  1340  	params := uri.Query()
  1341  	if len(params.Get("client_secret")) > 0 {
  1342  		params.Set("client_secret", "REDACTED")
  1343  		uri.RawQuery = params.Encode()
  1344  	}
  1345  	return uri
  1346  }
  1347  
  1348  /*
  1349  An Error reports more details on an individual error in an ErrorResponse.
  1350  These are the possible validation error codes:
  1351  
  1352  	missing:
  1353  	    resource does not exist
  1354  	missing_field:
  1355  	    a required field on a resource has not been set
  1356  	invalid:
  1357  	    the formatting of a field is invalid
  1358  	already_exists:
  1359  	    another resource has the same valid as this field
  1360  	custom:
  1361  	    some resources return this (e.g. github.User.CreateKey()), additional
  1362  	    information is set in the Message field of the Error
  1363  
  1364  GitHub error responses structure are often undocumented and inconsistent.
  1365  Sometimes error is just a simple string (Issue #540).
  1366  In such cases, Message represents an error message as a workaround.
  1367  
  1368  GitHub API docs: https://docs.github.com/rest/#client-errors
  1369  */
  1370  type Error struct {
  1371  	Resource string `json:"resource"` // resource on which the error occurred
  1372  	Field    string `json:"field"`    // field on which the error occurred
  1373  	Code     string `json:"code"`     // validation error code
  1374  	Message  string `json:"message"`  // Message describing the error. Errors with Code == "custom" will always have this set.
  1375  }
  1376  
  1377  func (e *Error) Error() string {
  1378  	return fmt.Sprintf("%v error caused by %v field on %v resource",
  1379  		e.Code, e.Field, e.Resource)
  1380  }
  1381  
  1382  func (e *Error) UnmarshalJSON(data []byte) error {
  1383  	type aliasError Error // avoid infinite recursion by using type alias.
  1384  	if err := json.Unmarshal(data, (*aliasError)(e)); err != nil {
  1385  		return json.Unmarshal(data, &e.Message) // data can be json string.
  1386  	}
  1387  	return nil
  1388  }
  1389  
  1390  // CheckResponse checks the API response for errors, and returns them if
  1391  // present. A response is considered an error if it has a status code outside
  1392  // the 200 range or equal to 202 Accepted.
  1393  // API error responses are expected to have response
  1394  // body, and a JSON response body that maps to ErrorResponse.
  1395  //
  1396  // The error type will be *RateLimitError for rate limit exceeded errors,
  1397  // *AcceptedError for 202 Accepted status codes,
  1398  // *TwoFactorAuthError for two-factor authentication errors,
  1399  // and *RedirectionError for redirect status codes (only happens when ignoring redirections).
  1400  func CheckResponse(r *http.Response) error {
  1401  	if r.StatusCode == http.StatusAccepted {
  1402  		return &AcceptedError{}
  1403  	}
  1404  	if c := r.StatusCode; 200 <= c && c <= 299 {
  1405  		return nil
  1406  	}
  1407  
  1408  	errorResponse := &ErrorResponse{Response: r}
  1409  	data, err := io.ReadAll(r.Body)
  1410  	if err == nil && data != nil {
  1411  		err = json.Unmarshal(data, errorResponse)
  1412  		if err != nil {
  1413  			// reset the response as if this never happened
  1414  			errorResponse = &ErrorResponse{Response: r}
  1415  		}
  1416  	}
  1417  	// Re-populate error response body because GitHub error responses are often
  1418  	// undocumented and inconsistent.
  1419  	// Issue #1136, #540.
  1420  	r.Body = io.NopCloser(bytes.NewBuffer(data))
  1421  	switch {
  1422  	case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"):
  1423  		return (*TwoFactorAuthError)(errorResponse)
  1424  	case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0":
  1425  		return &RateLimitError{
  1426  			Rate:     parseRate(r),
  1427  			Response: errorResponse.Response,
  1428  			Message:  errorResponse.Message,
  1429  		}
  1430  	case r.StatusCode == http.StatusForbidden &&
  1431  		(strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits") ||
  1432  			strings.HasSuffix(errorResponse.DocumentationURL, "secondary-rate-limits")):
  1433  		abuseRateLimitError := &AbuseRateLimitError{
  1434  			Response: errorResponse.Response,
  1435  			Message:  errorResponse.Message,
  1436  		}
  1437  		if retryAfter := parseSecondaryRate(r); retryAfter != nil {
  1438  			abuseRateLimitError.RetryAfter = retryAfter
  1439  		}
  1440  		return abuseRateLimitError
  1441  	// Check that the status code is a redirection and return a sentinel error that can be used to handle special cases
  1442  	// where 302 is considered a successful result.
  1443  	// This should never happen with the default `CheckRedirect`, because it would return a `url.Error` that should be handled upstream.
  1444  	case r.StatusCode == http.StatusMovedPermanently ||
  1445  		r.StatusCode == http.StatusFound ||
  1446  		r.StatusCode == http.StatusSeeOther ||
  1447  		r.StatusCode == http.StatusTemporaryRedirect ||
  1448  		r.StatusCode == http.StatusPermanentRedirect:
  1449  
  1450  		locationStr := r.Header.Get("Location")
  1451  		var location *url.URL
  1452  		if locationStr != "" {
  1453  			location, _ = url.Parse(locationStr)
  1454  		}
  1455  		return &RedirectionError{
  1456  			Response:   errorResponse.Response,
  1457  			StatusCode: r.StatusCode,
  1458  			Location:   location,
  1459  		}
  1460  	default:
  1461  		return errorResponse
  1462  	}
  1463  }
  1464  
  1465  // parseBoolResponse determines the boolean result from a GitHub API response.
  1466  // Several GitHub API methods return boolean responses indicated by the HTTP
  1467  // status code in the response (true indicated by a 204, false indicated by a
  1468  // 404). This helper function will determine that result and hide the 404
  1469  // error if present. Any other error will be returned through as-is.
  1470  func parseBoolResponse(err error) (bool, error) {
  1471  	if err == nil {
  1472  		return true, nil
  1473  	}
  1474  
  1475  	if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound {
  1476  		// Simply false. In this one case, we do not pass the error through.
  1477  		return false, nil
  1478  	}
  1479  
  1480  	// some other real error occurred
  1481  	return false, err
  1482  }
  1483  
  1484  type RateLimitCategory uint8
  1485  
  1486  const (
  1487  	CoreCategory RateLimitCategory = iota
  1488  	SearchCategory
  1489  	GraphqlCategory
  1490  	IntegrationManifestCategory
  1491  	SourceImportCategory
  1492  	CodeScanningUploadCategory
  1493  	ActionsRunnerRegistrationCategory
  1494  	ScimCategory
  1495  	DependencySnapshotsCategory
  1496  	CodeSearchCategory
  1497  	AuditLogCategory
  1498  
  1499  	Categories // An array of this length will be able to contain all rate limit categories.
  1500  )
  1501  
  1502  // GetRateLimitCategory returns the rate limit RateLimitCategory of the endpoint, determined by HTTP method and Request.URL.Path.
  1503  func GetRateLimitCategory(method, path string) RateLimitCategory {
  1504  	switch {
  1505  	// https://docs.github.com/rest/rate-limit#about-rate-limits
  1506  	default:
  1507  		// NOTE: coreCategory is returned for actionsRunnerRegistrationCategory too,
  1508  		// because no API found for this category.
  1509  		return CoreCategory
  1510  
  1511  	// https://docs.github.com/en/rest/search/search#search-code
  1512  	case strings.HasPrefix(path, "/search/code") &&
  1513  		method == http.MethodGet:
  1514  		return CodeSearchCategory
  1515  
  1516  	case strings.HasPrefix(path, "/search/"):
  1517  		return SearchCategory
  1518  	case path == "/graphql":
  1519  		return GraphqlCategory
  1520  	case strings.HasPrefix(path, "/app-manifests/") &&
  1521  		strings.HasSuffix(path, "/conversions") &&
  1522  		method == http.MethodPost:
  1523  		return IntegrationManifestCategory
  1524  
  1525  	// https://docs.github.com/rest/migrations/source-imports#start-an-import
  1526  	case strings.HasPrefix(path, "/repos/") &&
  1527  		strings.HasSuffix(path, "/import") &&
  1528  		method == http.MethodPut:
  1529  		return SourceImportCategory
  1530  
  1531  	// https://docs.github.com/rest/code-scanning#upload-an-analysis-as-sarif-data
  1532  	case strings.HasSuffix(path, "/code-scanning/sarifs"):
  1533  		return CodeScanningUploadCategory
  1534  
  1535  	// https://docs.github.com/enterprise-cloud@latest/rest/scim
  1536  	case strings.HasPrefix(path, "/scim/"):
  1537  		return ScimCategory
  1538  
  1539  	// https://docs.github.com/en/rest/dependency-graph/dependency-submission#create-a-snapshot-of-dependencies-for-a-repository
  1540  	case strings.HasPrefix(path, "/repos/") &&
  1541  		strings.HasSuffix(path, "/dependency-graph/snapshots") &&
  1542  		method == http.MethodPost:
  1543  		return DependencySnapshotsCategory
  1544  
  1545  	// https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/orgs?apiVersion=2022-11-28#get-the-audit-log-for-an-organization
  1546  	case strings.HasSuffix(path, "/audit-log"):
  1547  		return AuditLogCategory
  1548  	}
  1549  }
  1550  
  1551  // RateLimits returns the rate limits for the current client.
  1552  //
  1553  // Deprecated: Use RateLimitService.Get instead.
  1554  func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) {
  1555  	return c.RateLimit.Get(ctx)
  1556  }
  1557  
  1558  func setCredentialsAsHeaders(req *http.Request, id, secret string) *http.Request {
  1559  	// To set extra headers, we must make a copy of the Request so
  1560  	// that we don't modify the Request we were given. This is required by the
  1561  	// specification of http.RoundTripper.
  1562  	//
  1563  	// Since we are going to modify only req.Header here, we only need a deep copy
  1564  	// of req.Header.
  1565  	convertedRequest := new(http.Request)
  1566  	*convertedRequest = *req
  1567  	convertedRequest.Header = make(http.Header, len(req.Header))
  1568  
  1569  	for k, s := range req.Header {
  1570  		convertedRequest.Header[k] = append([]string(nil), s...)
  1571  	}
  1572  	convertedRequest.SetBasicAuth(id, secret)
  1573  	return convertedRequest
  1574  }
  1575  
  1576  /*
  1577  UnauthenticatedRateLimitedTransport allows you to make unauthenticated calls
  1578  that need to use a higher rate limit associated with your OAuth application.
  1579  
  1580  	t := &github.UnauthenticatedRateLimitedTransport{
  1581  		ClientID:     "your app's client ID",
  1582  		ClientSecret: "your app's client secret",
  1583  	}
  1584  	client := github.NewClient(t.Client())
  1585  
  1586  This will add the client id and secret as a base64-encoded string in the format
  1587  ClientID:ClientSecret and apply it as an "Authorization": "Basic" header.
  1588  
  1589  See https://docs.github.com/rest/#unauthenticated-rate-limited-requests for
  1590  more information.
  1591  */
  1592  type UnauthenticatedRateLimitedTransport struct {
  1593  	// ClientID is the GitHub OAuth client ID of the current application, which
  1594  	// can be found by selecting its entry in the list at
  1595  	// https://github.com/settings/applications.
  1596  	ClientID string
  1597  
  1598  	// ClientSecret is the GitHub OAuth client secret of the current
  1599  	// application.
  1600  	ClientSecret string
  1601  
  1602  	// Transport is the underlying HTTP transport to use when making requests.
  1603  	// It will default to http.DefaultTransport if nil.
  1604  	Transport http.RoundTripper
  1605  }
  1606  
  1607  // RoundTrip implements the RoundTripper interface.
  1608  func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  1609  	if t.ClientID == "" {
  1610  		return nil, errors.New("t.ClientID is empty")
  1611  	}
  1612  	if t.ClientSecret == "" {
  1613  		return nil, errors.New("t.ClientSecret is empty")
  1614  	}
  1615  
  1616  	req2 := setCredentialsAsHeaders(req, t.ClientID, t.ClientSecret)
  1617  	// Make the HTTP request.
  1618  	return t.transport().RoundTrip(req2)
  1619  }
  1620  
  1621  // Client returns an *http.Client that makes requests which are subject to the
  1622  // rate limit of your OAuth application.
  1623  func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client {
  1624  	return &http.Client{Transport: t}
  1625  }
  1626  
  1627  func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper {
  1628  	if t.Transport != nil {
  1629  		return t.Transport
  1630  	}
  1631  	return http.DefaultTransport
  1632  }
  1633  
  1634  // BasicAuthTransport is an http.RoundTripper that authenticates all requests
  1635  // using HTTP Basic Authentication with the provided username and password. It
  1636  // additionally supports users who have two-factor authentication enabled on
  1637  // their GitHub account.
  1638  type BasicAuthTransport struct {
  1639  	Username string // GitHub username
  1640  	Password string // GitHub password
  1641  	OTP      string // one-time password for users with two-factor auth enabled
  1642  
  1643  	// Transport is the underlying HTTP transport to use when making requests.
  1644  	// It will default to http.DefaultTransport if nil.
  1645  	Transport http.RoundTripper
  1646  }
  1647  
  1648  // RoundTrip implements the RoundTripper interface.
  1649  func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  1650  	req2 := setCredentialsAsHeaders(req, t.Username, t.Password)
  1651  	if t.OTP != "" {
  1652  		req2.Header.Set(headerOTP, t.OTP)
  1653  	}
  1654  	return t.transport().RoundTrip(req2)
  1655  }
  1656  
  1657  // Client returns an *http.Client that makes requests that are authenticated
  1658  // using HTTP Basic Authentication.
  1659  func (t *BasicAuthTransport) Client() *http.Client {
  1660  	return &http.Client{Transport: t}
  1661  }
  1662  
  1663  func (t *BasicAuthTransport) transport() http.RoundTripper {
  1664  	if t.Transport != nil {
  1665  		return t.Transport
  1666  	}
  1667  	return http.DefaultTransport
  1668  }
  1669  
  1670  // formatRateReset formats d to look like "[rate reset in 2s]" or
  1671  // "[rate reset in 87m02s]" for the positive durations. And like "[rate limit was reset 87m02s ago]"
  1672  // for the negative cases.
  1673  func formatRateReset(d time.Duration) string {
  1674  	isNegative := d < 0
  1675  	if isNegative {
  1676  		d *= -1
  1677  	}
  1678  	secondsTotal := int(0.5 + d.Seconds())
  1679  	minutes := secondsTotal / 60
  1680  	seconds := secondsTotal - minutes*60
  1681  
  1682  	var timeString string
  1683  	if minutes > 0 {
  1684  		timeString = fmt.Sprintf("%dm%02ds", minutes, seconds)
  1685  	} else {
  1686  		timeString = fmt.Sprintf("%ds", seconds)
  1687  	}
  1688  
  1689  	if isNegative {
  1690  		return fmt.Sprintf("[rate limit was reset %v ago]", timeString)
  1691  	}
  1692  	return fmt.Sprintf("[rate reset in %v]", timeString)
  1693  }
  1694  
  1695  func sleepUntilResetWithBuffer(ctx context.Context, reset time.Time) error {
  1696  	buffer := time.Second
  1697  	timer := time.NewTimer(time.Until(reset) + buffer)
  1698  	select {
  1699  	case <-ctx.Done():
  1700  		if !timer.Stop() {
  1701  			<-timer.C
  1702  		}
  1703  		return ctx.Err()
  1704  	case <-timer.C:
  1705  	}
  1706  	return nil
  1707  }
  1708  
  1709  // When using roundTripWithOptionalFollowRedirect, note that it
  1710  // is the responsibility of the caller to close the response body.
  1711  func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, maxRedirects int, opts ...RequestOption) (*http.Response, error) {
  1712  	req, err := c.NewRequest("GET", u, nil, opts...)
  1713  	if err != nil {
  1714  		return nil, err
  1715  	}
  1716  
  1717  	var resp *http.Response
  1718  	// Use http.DefaultTransport if no custom Transport is configured
  1719  	req = withContext(ctx, req)
  1720  	if c.client.Transport == nil {
  1721  		resp, err = http.DefaultTransport.RoundTrip(req)
  1722  	} else {
  1723  		resp, err = c.client.Transport.RoundTrip(req)
  1724  	}
  1725  	if err != nil {
  1726  		return nil, err
  1727  	}
  1728  
  1729  	// If redirect response is returned, follow it
  1730  	if maxRedirects > 0 && resp.StatusCode == http.StatusMovedPermanently {
  1731  		_ = resp.Body.Close()
  1732  		u = resp.Header.Get("Location")
  1733  		resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, maxRedirects-1, opts...)
  1734  	}
  1735  	return resp, err
  1736  }
  1737  
  1738  // Ptr is a helper routine that allocates a new T value
  1739  // to store v and returns a pointer to it.
  1740  func Ptr[T any](v T) *T {
  1741  	return &v
  1742  }
  1743  
  1744  // Bool is a helper routine that allocates a new bool value
  1745  // to store v and returns a pointer to it.
  1746  //
  1747  // Deprecated: use Ptr instead.
  1748  func Bool(v bool) *bool { return &v }
  1749  
  1750  // Int is a helper routine that allocates a new int value
  1751  // to store v and returns a pointer to it.
  1752  //
  1753  // Deprecated: use Ptr instead.
  1754  func Int(v int) *int { return &v }
  1755  
  1756  // Int64 is a helper routine that allocates a new int64 value
  1757  // to store v and returns a pointer to it.
  1758  //
  1759  // Deprecated: use Ptr instead.
  1760  func Int64(v int64) *int64 { return &v }
  1761  
  1762  // String is a helper routine that allocates a new string value
  1763  // to store v and returns a pointer to it.
  1764  //
  1765  // Deprecated: use Ptr instead.
  1766  func String(v string) *string { return &v }
  1767  
  1768  // roundTripperFunc creates a RoundTripper (transport).
  1769  type roundTripperFunc func(*http.Request) (*http.Response, error)
  1770  
  1771  func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
  1772  	return fn(r)
  1773  }
  1774  
  1775  var runIDFromURLRE = regexp.MustCompile(`^repos/.*/actions/runs/(\d+)/deployment_protection_rule$`)
  1776  
  1777  // GetRunID is a Helper Function used to extract the workflow RunID from the *DeploymentProtectionRuleEvent.DeploymentCallBackURL.
  1778  func (e *DeploymentProtectionRuleEvent) GetRunID() (int64, error) {
  1779  	match := runIDFromURLRE.FindStringSubmatch(*e.DeploymentCallbackURL)
  1780  	if len(match) != 2 {
  1781  		return -1, errors.New("no match")
  1782  	}
  1783  	runID, err := strconv.ParseInt(match[1], 10, 64)
  1784  	if err != nil {
  1785  		return -1, err
  1786  	}
  1787  	return runID, nil
  1788  }