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