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