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