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