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