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