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