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