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